无上大圆哈头像
哈尔滨工业大学计算机系统HIT-ICS大作业:程序人生-Hello’s P2P封面图

哈尔滨工业大学计算机系统HIT-ICS大作业:程序人生-Hello’s P2P

摘  要

本报告以老师提供的非常具有代表性的hello.c程序为例,分析了一个C语言程序从源代码到进程执行的完整生命周期。通过深入分析程序的预处理、编译、汇编、链接、进程管理和存储管理等关键环节,探究了计算机系统底层的工作机制。撰写本报告的过程中采用Ubuntu 20.04作为实验环境,结合GCC工具链和多种调试分析工具,详细记录了程序在各阶段的转换过程与中间结果。特别探讨了ELF文件格式、虚拟地址空间管理、动态链接机制、进程调度策略以及内存管理技术等核心概念。实践探究结果印证了现代计算机系统通过精妙的分层抽象和高效的资源管理机制,来实现程序的可靠执行和系统资源的高效利用,不仅加深了对计算机系统工作原理的理解,也为后续的系统级编程和性能优化提供了理论基础和实践指导。

关键词:计算机系统;程序生命周期;ELF格式;进程管理;存储管理;动态链接

第1章 概述

1.1 Hello简介

Hello程序是一个简单的C语言程序,它展示了程序从源代码到进程执行的完整生命周期,即"From Program to Process"(P2P)的过程,以及从零开始到最终结束的"From Zero-0 to Zero-0"(020)过程。

P2P过程分析:

Program阶段:

  1. 程序员创建hello.c源文件,包含main函数和必要的头文件。
  2. 预处理阶段:处理#include和#define等预处理指令,生成hello.i文件。
  3. 编译阶段:将预处理后的C代码转换为汇编代码(hello.s)。
  4. 汇编阶段:将汇编代码转换为机器码,生成可重定位目标文件(hello.o)。
  5. 链接阶段:将目标文件与系统库链接,生成可执行文件(hello)。

Process阶段:

  1. Shell通过fork()创建新进程。
  2. execve()加载并执行hello程序。
  3. CPU执行程序指令,输出问候信息。
  4. 程序终止,进程被操作系统回收。

020过程分析:

  1. From Zero:程序从无到有,经过编译链接过程生成可执行文件。
  2. 执行阶段:程序运行时创建进程,在屏幕上输出指定格式的信息。
  3. To Zero:程序执行完毕,进程终止,所有资源被系统回收,回归初始状态。

1.2 环境与工具

硬件环境:

处理器:AMD Ryzen 7 7745HX with Radeon Graphics-3.60 GHz

内存:16.0GB(机器的全部内存);4GB RAM(Ubuntu虚拟机可用的内存空间)

存储:2TB(机器的全部硬盘空间)20GB SSD(分配给Ubuntu虚拟机的硬盘空间)

软件环境:

操作系统:Ubuntu 20.04.6 LTS

内核版本:Linux 5.15.0-134-generic

开发与调试工具:

GCC工具链:

gcc 11.3.0:用于编译和链接程序

编译命令:gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC hello.c -o hello

调试工具:

  1. GDB 12.1:用于程序调试
  2. EDB调试器:用于分析程序执行过程

分析工具:

  1. objdump:用于反汇编和目标文件分析
  2. readelf:用于分析ELF格式文件
  3. strace:用于跟踪系统调用

1.3 中间结果

在Hello程序的生成和执行过程中,会产生以下中间文件:

hello.i:预处理后的源代码文件

作用:展示宏展开和头文件包含后的完整代码

生成命令:gcc -E hello.c -o hello.i

hello.s:汇编代码文件

作用:展示C代码转换后的汇编语言表示

生成命令:gcc -S hello.i -o hello.s

hello.o:可重定位目标文件

作用:包含机器码但未完成最终地址重定位

生成命令:gcc -c hello.s -o hello.o

hello:最终可执行文件

作用:可直接在Linux系统上运行的程序

生成命令:gcc hello.o -o hello

1.4 本章小结

本章概述了Hello程序的生命周期(P2P和020过程),介绍了完成本大作业所使用的软硬件环境和工具链,并列举了在程序构建过程中生成的中间文件及其作用。通过本章的介绍,我们对Hello程序有了整体认识,为后续章节深入分析程序的预处理、编译、汇编、链接、进程管理和存储管理等细节奠定了基础。接下来的章节将逐步深入分析Hello程序在计算机系统中的完整执行过程。

第2章 预处理

2.1 预处理的概念与作用

预处理(Preprocessing)是C程序编译的第一个阶段,由预处理器(cpp)负责处理源代码中的预处理指令(以#开头的指令)。其主要作用包括:

  1. 宏展开:替换#define定义的宏。
  2. 头文件包含:将#include指定的头文件内容插入到源文件中。
  3. 条件编译:根据#if、#ifdef、#ifndef等条件编译指令选择性地包含或排除代码。
  4. 删除注释:移除所有注释(/* ... */ 和 // ...)。
  5. 添加行号和文件名标记:便于后续编译阶段的错误定位。

预处理后的文件(.i后缀的文件)仍然是纯文本文件,但已经去除了预处理指令,并完成了宏替换和头文件展开。

2.2在Ubuntu下预处理的命令

在Ubuntu中,可以使用GCC的-E选项进行预处理:

gcc -E hello.c -o hello.i 

其中,-E表示仅执行预处理,不进行编译、汇编和链接;-o hello.i表示指定输出文件名为hello.i。

在Ubuntu终端执行此命令的结果如下图所示:

图2.1:Ubuntu下执行预处理命令前后截图

可以看到,在没有执行命令行时,该目录下只有hello.c一个文件;执行过预处理命令后,便多了生成的预处理文件:hello.i。同时也可以看到在当前目录的文件夹里出现了所预期的hello.i文件。

2.3 Hello的预处理结果解析

打开生成的hello.i文件,可见原本的c程序被预处理成为了3060行代码:

图2.2:查看hello.i的结果

可以观察到,c程序里的代码部分在预处理后并没有变化,只占了3060行预处理代码中的14行。前面的大量代码,都源于头文件<stdio.h>  <unistd.h>  <stdlib.h> 的依次展开。

以 stdio.h 的展开为例:预处理过程中,#include指令的作用是把指定的头文件的内容包含到源文件中。stdio.h是标准输入输出库的头文件,它包含了用于读写文件、标准输入输出的函数原型和宏定义等内容。

当预处理器遇到#include<stdio.h>时,它会在系统的头文件路径下查找stdio.h文件,一般在/usr/include目录下,然后把stdio.h文件中的内容复制到源文件中。stdio.h文件中可能还有其他的#include指令,比如#include<stddef.h>或#include<features.h>等,这些头文件也会被递归地展开到源文件中。

预处理器不会对头文件中的内容做任何计算或转换,只是简单地复制和替换。

在阅读预处理得来的hello.i的内容时候,也可以发现并印证一些预处理过程中会对原始代码做出的改变。下面以截图举例说明:

首先就是头文件的展开:

图2.3:头文件展开截图示例

可以看到,一些库函数里的功能函数,比如fflush和相关的FILE操作函数,都被展开成了如上图所示的以extern为开头的声明格式。

然后就是注释删除:

图2.4:注释删除截图示例

可以看到,预处理文件在头文件展开代码结束后紧跟着hello.c中的代码部分,原有的一些注释被全部删除了,这符合我们对预处理过程的认知。

2.4 本章小结

本章讲述了在Linux环境中,如何用命令对c程序进行预处理,以及预处理的含义和作用。然后用一个简单的hello程序演示了从hello.c到hello.i的过程,并对所生成的hello.i文件进行了查看和分析。通过对比观察,可以分析得到预处理过程会将引用的头文件全部展开为所引用的库文件内容,并且预处理文件会包含一些宏和常量的定义,还有一些行号信息和条件编译指令,同时也会把c程序中的注释部分全部删除。

第3章 编译

3.1 编译的概念与作用

编译(Compilation)是将预处理后的C代码(.i文件)转换为汇编代码(.s文件)的过程。编译器(如GCC)在此阶段执行以下关键操作:

  1. 词法分析:将源代码分解为词法单元(tokens)。
  2. 语法分析:构建抽象语法树(AST),检查语法正确性。
  3. 语义分析:验证类型、作用域等语义规则。
  4. 中间代码生成:生成与机器无关的中间表示(如RTL)。
  5. 代码优化:对中间代码进行优化(如常量传播、死代码消除)。
  6. 目标代码生成:将优化后的中间代码转换为目标机器的汇编指令。

3.2 在Ubuntu下编译的命令

在Ubuntu中使用GCC生成汇编代码的命令如下:

gcc -S hello.i -o hello.s 

在Ubuntu终端执行此命令的结果如下图所示:

图3.1:Ubuntu下执行编译命令前后截图

可以看到,在没有执行命令行时,该目录下只有hello.c和hello.i两个文件;执行过编译命令后,便多了生成的汇编文件:hello.s。同时也可以看到在当前目录的文件夹里出现了所预期的hello.s文件。

3.3 Hello的编译结果解析

3.3.1 程序元信息和字符串常量

图3.2:汇编代码中的程序元信息和字符串常量部分

首先开头标识了源文件为hello.c,之后定义了两个字符串常量,存储在.rodata只读数据段:.LC0是参数错误提示信息,.LC1是printf的格式字符串。.LC0应为"用法: Hello 学号 姓名 手机号 秒数!",此处是汉字,呈现为UTF-8编码的中文字符串的八进制转义序列表示,因为在汇编文件(.s)中,GCC编译器会将非ASCII字符(如中文)转换为八进制转义序列(\后跟3位八进制数字),以确保汇编器能正确解析。

3.3.2 函数框架

图3.3:汇编代码中的函数框架部分

此部分设置了函数栈帧,分配32字节栈空间;endbr64是安全特性指令,防止控制流劫持。

3.3.3 参数检查

图3.4:汇编代码中的参数检查部分

此部分使用cmpl指令比较参数数量,并通过PLT(过程链接表)调用库函数。

3.3.4 主循环结构

此部分使用jmp和条件跳转实现for循环,通过指针算术运算访问argv数组元素,并在每次循环调用printf和sleep函数。对于具体代码的分析见下图:

图3.5:汇编代码中的主循环结构部分

3.3.5 函数返回

图3.6:汇编代码中的函数返回部分

此部分调用getchar等待输入,设置返回值0,然后恢复栈帧并返回。

以上我们是从汇编代码的整体结构入手,分析每一段汇编代码对应什么作用,下面我们将进一步把汇编代码按着数据处理,函数调用,赋值、算术、关系等运算以及控制跳转和类型转换等具体操作进行分析。

3.3.6 数据与类型处理

(1)常量处理

图3.7:汇编代码中的常量处理部分

字符串常量使用.string指令定义,存储在只读数据段(.rodata),编译器将中文字符转换为UTF-8编码的八进制转义序列。

(2)变量处理

图3.8:汇编代码中的变量处理部分

这两行汇编代码分别将argc和argv存储到栈。

(3)类型转换

图3.9:汇编代码中的类型转换部分

atoi实现显式的字符串到整数转换,movl指令处理32位到64位的隐式扩展。

3.3.7 表达式与操作符

(1)算数运算

图3.10:汇编代码中的算数运算(其一)

此条语句是通过addq实现指针运算;

图3.11:汇编代码中的算数运算(其二)

而此条语句是使用addl实现整数自增,他们都是算术运算的体现。

(2)关系运算

图3.12:汇编代码中的关系运算

这两句是将argc与5比较,若不相等,则跳转到.L2。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2301_80741306/article/details/147569013

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--