题 目 程序人生-Hello’s P2P
专 业 航天学院 人工智能(未来技术)
学 号 7203610101
班 级 2036015
学 生 周子康
指 导 教 师 史先俊
本论文将CSAPP结合课程内容hello研究小程序的生活hello.c这个简单的c语言文件在于Linux全面梳理和回顾系统下的整个生命周期。编译、链接、加载、对编译、链接、加载、操作、终止、回收的过程进行了深入的研究,以了解hello.c文件的一生。该论文以hello.c文件是研究对象,结合对计算机系统的深入理解和课堂教师的教学,Ubuntu下对hello通过研究程序的整个生命周期,研究了程序hello.c通过对程序的深入研究,可以将整个计算机系统系列在一起,真正实现学以致用、融合贯通。
hello;程序生命周期;计算机系统;计算机系统结构;
1.1 Hello简介... - 4 -
1.2 环境与工具... - 4 -
1.3 中间结果... - 4 -
1.4 本章小结... - 5 -
2.1 预处理的概念和作用... - 6 -
2.2在Ubuntu下一个预处理命令... - 6 -
2.3 Hello预处理结果分析................. - 7 -
2.4 本章小结......................................................................................................... - 7 -
3.1 编译的概念与作用......................................................................................... - 9 -
3.2 在Ubuntu下编译的命令............................................................................. - 9 -
3.3 Hello的编译结果解析.................................................................................. - 9 -
3.4 本章小结....................................................................................................... - 21 -
4.1 汇编的概念与作用....................................................................................... - 22 -
4.2 在Ubuntu下汇编的命令........................................................................... - 22 -
4.3 可重定位目标elf格式............................................................................... - 22 -
4.4 Hello.o的结果解析.................................................................................... - 26 -
4.5 本章小结....................................................................................................... - 29 -
5.1 链接的概念与作用....................................................................................... - 30 -
5.2 在Ubuntu下链接的命令........................................................................... - 30 -
5.3 可执行目标文件hello的格式.................................................................. - 30 -
5.4 hello的虚拟地址空间................................................................................ - 35 -
5.5 链接的重定位过程分析............................................................................... - 38 -
5.6 hello的执行流程........................................................................................ - 40 -
5.7 Hello的动态链接分析................................................................................ - 41 -
5.8 本章小结....................................................................................................... - 42 -
6.1 进程的概念与作用....................................................................................... - 43 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 43 -
6.3 Hello的fork进程创建过程..................................................................... - 43 -
6.4 Hello的execve过程................................................................................. - 43 -
6.5 Hello的进程执行........................................................................................ - 44 -
6.6 hello的异常与信号处理............................................................................ - 44 -
6.7本章小结....................................................................................................... - 45 -
7.1 hello的存储器地址空间............................................................................ - 50 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................ - 50 -
7.3 Hello的线性地址到物理地址的变换-页式管理...................................... - 50 -
7.4 TLB与四级页表支持下的VA到PA的变换............................................. - 51 -
7.5 三级Cache支持下的物理内存访问.......................................................... - 52 -
7.6 hello进程fork时的内存映射.................................................................. - 53 -
7.7 hello进程execve时的内存映射.............................................................. - 54 -
7.8 缺页故障与缺页中断处理........................................................................... - 54 -
7.9动态存储分配管理....................................................................................... - 54 -
7.10本章小结..................................................................................................... - 55 -
8.1 Linux的IO设备管理方法.......................................................................... - 57 -
8.2 简述Unix IO接口及其函数....................................................................... - 57 -
8.3 printf的实现分析........................................................................................ - 57 -
8.4 getchar的实现分析.................................................................................... - 58 -
8.5本章小结....................................................................................................... - 61 -
第1章 概述
1.1 Hello简介
Hello的P2P是指hello.c文件从可执行程序(Program)变为运行时进程(Process)的过程。在Linux系统下,hello.c 文件依次经过cpp(C Pre-Processor,C预处理器)预处理、ccl(C Compiler,C编译器) 编译、as (Assembler,汇编器)汇编、ld (Linker,链接器)链接最终成为可执行目标程序hello(在Linux下该文件无固定后缀)。打开shell,输入命令./hello后,shell 通过fork产生子进程,hello 便从可执行程序(Program)变成为进程(Process)。
Hello的020是指hello.c文件“From 0 to 0”,初始时内存中并无hello文件的相关内容,这便是“From 0”。通过在Shell下调用execve函数,系统会将hello文件载入内存,执行相关代码,当程序运行结束后, hello进程被回收,并由内核删除hello相关数据,这即为“to 0”。
1.2 环境与工具
硬件环境:处理器:Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz 2.59 GHz
RAM:16.0 GB (15.8 GB 可用)
系统类型:64位操作系统,基于x64的处理器
软件环境:Windows10 64位;Ubuntu 20.04.1
开发与调试工具:Visual Studio 2019,gedit,gcc,notepad++,readelf, objdump,hexedit,edb
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
表格 1 中间结果
| 文件名 |
功能 |
| hello.i |
预处理后得到的文本文件 |
| hello.s |
编译后得到的汇编语言文件 |
| hello.o |
汇编后得到的可重定位目标文件 |
| hello.elf |
用readelf读取hello.o得到的ELF格式信息 |
| hello.asm |
反汇编hello.o得到的反汇编文件 |
| hello1.elf |
由hello可执行文件生成的.elf文件 |
| hello1.asm |
反汇编hello可执行文件得到的反汇编文件 |
1.4 本章小结
本章对hello进行了一个总体的概括,简要介绍了hello 的P2P,020的具体含义,同时列出了研究时采用的具体软硬件环境和中间结果。
第2章 预处理
2.1 预处理的概念与作用
预处理中会展开以#起始的行,试图解释为预处理指令(preprocessing directive) ,其中 ISO C/C++要求支持的包括#if、 #ifdef、 #ifndef、 #else、 #elif、 #endif(条件编译)、 #define(宏定义)、 #include(源文件包含)、 #line(行控制)、 #error(错误指令)、 #pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。
- 将源文件中用#include 形式声明的文件复制到新的程序中。比如 hello.c中的#include<stdio.h> 等命令告诉预处理器读取系统头文件 stdio.h unistd.h stdlib.h 的内容,并把它直接插入到程序文本中。
- 用实际值替换用#define 定义的字符串
- 根据#if 后面的条件决定需要编译的代码
- 特殊符号,预编译程序可以识别一些特殊的符号, 预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
注:预处理过程中并未直接解析程序源代码的内容,而是对源代码进行相应的分割、处理和替换。简单来说,预处理是一个文本插入与替换的过程,生成的hello.i文件仍然是文本文件。
2.2在Ubuntu下预处理的命令
在Ubuntu下,进行预处理的命令为:
cpp hello.c > hello.i
运行截图如下:
正在上传…重新上传取消
图 1 预处理过程
2.3 Hello的预处理结果解析
在Linux下打开hello.i文件,可以发现hello.i程序已经拓展为3060行,行数比起hello.c文件大幅增加。其中, hello.c中的main函数相关代码在hello.i程序中对应着3047行到3060行。
正在上传…重新上传取消
图 2 预处理结果部分展示
在main函数内代码出现之前是大段的头文件 stdio.h unistd.h stdlib.h 的依次展开。展开的具体流程概述如下(以stdio.h为例):CPP先删除指令#include <stdio.h>,并到Ubuntu系统的默认的环境变量中寻找 stdio.h,最终打开路径/usr/include/stdio.h下的stdio.h文件。若stdio.h文件中使用了#define语句,则按照上述流程继续递归地展开,直到所有#define语句都被解释替换掉为止。除此之外,CPP还会进行删除程序中的注释和多余的空白字符等操作,并对一些值进行替换。
2.4 本章小结
本章主要介绍了预处理的概念及作用、并结合Ubuntu系统下hello.c文件实际预处理之后得到的hello.i程序对预处理结果进行了解析,详细了解了预处理的内涵。
第3章 编译
3.1 编译的概念与作用
编译程序所要做的工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。编译器将文本文件 hello.i 翻译成文本文件 hello.s。
- 语法分析:编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,方法分为两种:自上而下分析法和自下而上分析法。
- 中间代码:源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。
- 代码优化:指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。
- 目标代码:生成是编译的最后一个阶段。目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。此处指汇编语言代码,须经过汇编程序汇编后,成为可执行的机器语言代码。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
运行截图如下:
正在上传…重新上传取消
图 3 编译过程
3.3 Hello的编译结果解析
-
-
- 文件结构分析
-
对hello.s文件整体结构分析如下:
表格 2 hello.s文件结构
| 内容 |
含义 |
| .file |
源文件 |
| .text |
代码段 |
| .global |
全局变量 |
| .data |
存放已经初始化的全局和静态C 变量 |
| .section .rodata |
存放只读变量 |
| .align |
对齐方式 |
| .type |
表示是函数类型/对象类型 |
| .size |
表示大小 |
| .long .string |
表示是long类型/string类型 |
在if语句
正在上传…重新上传取消
中,常量4的值保存的位置在.text中,作为指令的一部分
正在上传…重新上传取消
同理可得
正在上传…重新上传取消
中的数字0、8、1、2、3也被存储在.text节中;
在下述函数中:
正在上传…重新上传取消
printf()、scanf()中的字符串则被存储在.rodata节中
正在上传…重新上传取消
字符串常量,储存在.text数据段中。\XXX为UTF-8编码,一个汉字对应三个字节。
初始化的全局变量储存在.data节,在.type段声明其为object类型,在.size段声明其长度,它的初始化不需要汇编语句,而是直接完成的。
编译器将局部变量存储在寄存器或者栈空间中。i作为函数内部的局部变量,并不占用文件实际节的空间,只存在于运行时栈中。对于i的操作就是直接对寄存器或栈进行操作。
在hello.s中我们可以看出,i占据了4字节的地址空间:
正在上传…重新上传取消
在汇编代码中
正在上传…重新上传取消
此处是循环前i=0的操作,i被保存在栈当中。
程序中涉及的数组为char *argv[],即函数的第二个参数。在hello.s中,其首地址保存在栈中。访问时,通过寄存器寻址的方式访问。
正在上传…重新上传取消
表格 3 mov指令的后缀
| 后缀 |
b |
w |
l |
q |
| 大小(字节) |
1 |
2 |
3 |
4 |
2. 初始化的全局变量储存在.data节,在.type段声明其为object类型,在.size段声明其长度,它的初始化不需要汇编语句,而是直接完成的。
-
-
- 算术操作
-
汇编语言中,算数操作的指令包括:
表格 4 算数操作指令
| 指令 |
效果 |
| leaq s,d |
d=&s |
| inc d |
d+=1 |
| dec d |
d-=1 |
| neg d |
d=-d |
| add s,d |
d=d+s |
| sub s,d |
d=d-s |
| imulq s |
r[%rdx]:r[%rax]=s*r[%rax](有符号) |
| mulq s |
r[%rdx]:r[%rax]=s*r[%rax](无符号) |
| idivq s |
r[%rdx]=r[%rdx]:r[%rax] mod s(有符号) r[%rax]=r[%rdx]:r[%rax] div s |
| divq s |
r[%rdx]=r[%rdx]:r[%rax] mod s(无符号) r[%rax]=r[%rdx]:r[%rax] div s |
在hello.s中,具体涉及的算数操作包括:
在hello.s中,具体涉及的关系操作包括:
- argc!=4:
检查argc是否不等于4。在hello.s中,使用cmpl $4,-20(%rbp),比较 argc与4的大小并设置条件码,为下一步je利用条件码进行跳转作准备。
正在上传…重新上传取消
图 4检查argc!=4
- i<8:
检查i是否小于8。在hello.s中,使用cmpl $7, -4(%rbp)比较i与7的大小,然后设置条件码,为下一步jle利用条件码进行跳转做准备。
正在上传…重新上传取消
图 5 检查i<8
主函数main的参数中有指针数组char *argv[]
正在上传…重新上传取消
在argv数组中,argv[0]指向输入程序的路径和名称,argv[1]和argv[2]分别表示两个字符串。
因为char* 数据类型占8个字节,根据
正在上传…重新上传取消
正在上传…重新上传取消
正在上传…重新上传取消
正在上传…重新上传取消对比原函数可知通过%rsi-8和%rax-16,分别得到argv[1]和argv[2]两个字符串。
-
-
- 控制转移
-
程序中控制转移的具体表现有两处:
- if(argc!=4):
当argc不等于4时,执行函数体内部的代码。在hello.s中,使用cmpl $4,-20(%rbp),比较 argc与4是否相等,若相等,则跳转至.L2,不执行后续部分内容;若不等则继续执行函数体内部对应的汇编代码。
正在上传…重新上传取消
图 6 控制转移
- for(i=0;i<8;i++):
当i < 8时进行循环,每次循环i++。在hello.s中,使用cmpl $7,-4 (%rbp),比较 i与7是否相等,在i<=7时继续循环,进入.L4,i>7时跳出循环。
正在上传…重新上传取消
图 7 循环的情况
C语言中,调用函数时进行的操作如下:
- 传递控制:
进行过程 Q 的时候,程序计数器必须设置为 Q 的代码的起始地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的地址。
- 传递数据:
P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P 中返回一个值。
- 分配和释放内存:
在开始时,Q 可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。
另附64位系统下的参数传递顺序:
表格 5 64位系统的传参顺序
| 1 |
2 |
3 |
4 |
5 |
6 |
7 |
| %rdi |
%rsi |
%rdx |
%rcx |
%r8 |
%r9 |
栈空间 |
具体到hello.s中,程序入口处,调用了main 函数,其在hello.s中标注为@function函数类型。之后又调用 puts,printf,sleep,exit,getchar 函数,对函数的调用都通过call指令进行。
参数传递:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储。
函数调用:被系统启动函数调用。
函数返回:设置%eax为0并且返回,对应return 0 。
源代码:
正在上传…重新上传取消
汇编代码:
正在上传…重新上传取消
可见argc存储在%edi中,argv存储在%rsi中;
参数传递:call puts时只传入了字符串参数首地址;for循环中call printf时传入了 argv[1]和argc[2]的地址。
函数调用:if判断满足条件后调用,与for循环中被调用。
源代码1:
正在上传…重新上传取消正在上传…重新上传取消
汇编代码1:
正在上传…重新上传取消正在上传…重新上传取消
正在上传…重新上传取消正在上传…重新上传取消
源代码2:
正在上传…重新上传取消正在上传…重新上传取消
汇编代码2:
参数传递:传入的参数为1,再执行退出命令
函数调用:if判断条件满足后被调用.
源代码:
正在上传…重新上传取消
汇编代码:
正在上传…重新上传取消正在上传…重新上传取消
参数传递:传入参数atoi(argv[3]),
函数调用:for循环下被调用,call sleep
源代码:
正在上传…重新上传取消正在上传…重新上传取消
汇编代码:
正在上传…重新上传取消正在上传…重新上传取消
函数调用:在main中被调用,call getchar
源代码:
正在上传…重新上传取消正在上传…重新上传取消
汇编代码:
正在上传…重新上传取消
3.4 本章小结
本章介绍了编译的概念与作用,编译是将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备的过程。同时,本章以hello.s文件为例,介绍了编译器如何处理各个数据类型以及各类操作,验证了大部分数据、操作在汇编代码中的实现。
第4章 汇编
4.1 汇编的概念与作用
汇编是指汇编器(assembler)将以.s结尾的汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在.o 目标文件中的过程
汇编器将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中,.o文件是一个二进制文件,它包含程序的指令编码。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
as hello.s -o hello.o
汇编过程如下:
正在上传…重新上传取消正在上传…重新上传取消
图 8 汇编的命令
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
首先,在shell中输入readelf -a hello.o > hello.elf 指令获得 hello.o 文件的 ELF 格式:
正在上传…重新上传取消正在上传…重新上传取消
图 9 生成ELF文件
其结构分析如下:
- ELF 头(ELF Header):
以 16字节序列 Magic 开始,其描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头大小、目标文件类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量等相关信息。
正在上传…重新上传取消正在上传…重新上传取消
图 10 ELF头的情况
- 节头:
包含了文件中出现的各个节的意义,包括节的类型、位置和大小等信息。
正在上传…重新上传取消正在上传…重新上传取消
图 11 节头的情况
- 重定位节.rela.text
一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
在这里,8 条重定位信息分别是对.L0(第一个 printf 中的字符串)、puts 函数、exit 函数、.L1(第二个 printf 中的字符串)、printf 函数、atoi、sleep 函数、getchar 函数进行重定位声明。
.rela.text节包含如下信息:
表格 6 .rela.text节包含的信息
| 偏移量 |
代表需要进行重定向的代码在.text或.data节中的偏移位置 |
| 信息 |
包括symbol和type两部分,其中symbol占前半部分,type占后半部分,symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的类型 |
| 类型 |
重定位到的目标的类型 |
| 加数 |
计算重定位位置的辅助信息 |
正在上传…重新上传取消正在上传…重新上传取消
图 12 .rela.text节
- 重定位节.rela.eh_frame
正在上传…重新上传取消正在上传…重新上传取消
图 13 .rela.eh_frame节
- 符号表Symbol table
符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。
图 14 符号表的情况
4.4 Hello.o的结果解析
使用objdump -d -r hello.o > hello.asm 分析hello.o的反汇编,并与第3章的 hello.s文件进行对照分析。
正在上传…重新上传取消正在上传…重新上传取消
图 15 生成hello.asm文件
通过对比hello.asm与hello.s可知,两者在如下地方存在差异:
- 分支转移:
在hello.s中,跳转指令的目标地址直接记为段名称,如.L2,.L3等。而在反汇编得到的hello.asm中,跳转的目标为具体的地址,在机器代码中体现为目标指令地址与当前指令下一条指令的地址之差。
正在上传…重新上传取消正在上传…重新上传取消
图 16 分支转移
- 函数调用:
在hello.s文件中,call之后直接跟着函数名称,而在反汇编得到的hello.asm中,call 的目标地址是当前指令的下一条指令。这是因为 hello.c 中调用的函数都是共享库中的函数,最终需要通过动态链接器作用才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其 call 指令后的相对地址设置为全0(此时,目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接进一步确定。
正在上传…重新上传取消正在上传…重新上传取消
图 17 函数调用
- 全局变量访问:
在hello.s 文件中,使用段名称+%rip访问 rodata(printf 中的字符串),而在反汇编得到的hello.asm中,使用 0+%rip进行访问。其原因与函数调用类似,rodata 中数据地址在运行时才能确定,故访问时也需要重定位。在汇编成为机器语言时,将操作数设置为全 0 并添加相应的重定位条目。
正在上传…重新上传取消正在上传…重新上传取消
图 18 全局变量访问
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.5 本章小结
本章介绍了汇编的概念与作用,在Ubuntu下通过实际操作将hello.s文件翻译为hello.o文件,并生成hello.o的ELF格式文件hello.elf,研究了ELF格式文件的具体结构。通过比较hello.o的反汇编代码(保存在hello.asm中)与hello.s中代码,
了解了汇编语言与机器语言的异同之处。
第5章 链接
5.1 链接的概念与作用
链接是指通过链接器(Linker),将