本文探究hello.c从c语言文件通过预处理、编译、汇编、链接等过程在Linux下运行的生命周期全过程。本文将CSAPP课堂讲授内容、书中涉及内容与具体实际操作相结合,通过详细且清晰的逐步分析过程,剖析了hello的一生,对于了解程序运行的底层原理将会有所帮助与启发。
关键词: 计算机系统;程序的一生;运行原理
目 录
6.2 简述壳Shell-bash的作用与处理流程... - 25 -
6.3 Hello的fork进程创建过程... - 25 -
7.6 hello进程fork时的内存映射... - 31 -
7.7 hello进程execve时的内存映射... - 31 -
第1章 概述
1.1 Hello简介
P2P:指的是From Program to Process,即从程序到过程。以Hello为例,一开始由程序员编写Hello.c源文件,由cpp预处理器读取头文件内容直接插入程序文本中形成Hello.i,编译器ccl将其翻译成汇编文本文件Hello.s,汇编器as会将其翻译成机器语言指令,形成可重定位目标程序Hello.o,最后通过链接器ld实现库函数合并,得到Hello可执行程序。在bash中通过fork创建子进程运行Hello。
O2O:指的是From Zero to Zero,即从无到无。在shell依次通过fork创建子进程、execve映射虚拟内存加载hello,CPU分配时间片进入逻辑控制流,完成取址、译码、执行、访存、写回、更新PC,通过IO管理输入输出。结束后,shell回收进程,释放虚拟地址空间,删除有关进程上下文。
1.2 环境与工具
硬件环境:
处理器 X86; 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
16G RAM,1T SSD Disk
软件环境:
系统版本 Windows 11 家庭中文版 21H2 22000.856
VMvare版本 VMware® Workstation 16 Pro 16.2.2 build-19200509
Ubuntu版本 22.04
开发工具:
CodeBlocks 64位;vi/vim/gedit+gcc;GDB
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
表一:中间结果文件名称及作用
文件名 | 功能 |
hello.i | 预处理后得到的文本文件 |
hello.s | 编译后得到的汇编语言文本文件 |
hello.o | 汇编后得到的可重定位目标文件 |
hello.elf | 用readelf读取hello.o得到的ELF格式信息 |
hello | Hello的可执行文件 |
helloexc.asm | 反汇编hello.o得到的反汇编文件 |
helloexc.elf | 由hello可执行文件生成的.elf文件 |
helloexc.asm | 反汇编hello可执行文件得到的反汇编文件 |
1.4 本章小结
本章根据Hello的自白,简述了Hello的一生,介绍了P2P、O2O的过程;列出了我使用的软硬件环境、开发与调试工具;列出了本文所生成的中间结果文件的名字,文件的作用。
第2章 预处理
2.1 预处理的概念与作用
预处理器(cpp)根据以字符#开头的命令,修改原始的源程序。常见的预处理指令包括#include,#define,#undef等。以#include<stdio.h>为例,预处理器会读取系统头文件stdio.h中的内容,并把它直接插入到程序文本中。
而其余常见指令还包括:#define表示宏定义,#undef表示撤销宏定义,#ifdef、#ifndef分别表示如果有/无定义。
2.2在Ubuntu下预处理的命令
命令:gcc -E -o hello.i hello.c,-E是生成预处理文件的指令。
图2.1 预处理指令
执行后目录下将出现hello.i文件。
2.3 Hello的预处理结果解析
预处理后仍为文本文件,因此可以直接打开。可以看到,预处理文本文件非常之长,达到了3091行,其中原始的main主程序出现在了最下方。
图2.2 main主程序
而主程序之前主要是将stdio.h、unistd.h、stdlib.h三个头文件中的内容展开复制到该文本文件当中。以下是stdio.h中的部分内容展示。
图2.3 stdio.h部分内容
通过以上过程,我们可以了解到预处理后程序发生了什么变化。
2.4 本章小结
本小节介绍了预处理的概念及作用,展示了linux下预处理的指令以及对Hello.c预处理后的结果,并对结果进行了简要分析。
第3章 编译
3.1 编译的概念与作用
编译器将文本文件.i翻译成汇编程序语言文本文件.s。
编译即将预处理文本文件经过词法分析和语法分析等阶段,编译为机器语言,能够使计算机识别。此外,编译过程能够发现语法错误给出提示,不同的编译选项也会给程序不同级别的优化,汇编语言文本文件以文本的形式描述了机器语言指令。
3.2 在Ubuntu下编译的命令
命令:gcc -S -o hello.s hello.i,其中-S是生成汇编程序文本文件的指令。
图3.1 生成汇编程序文本文件指令
执行后,目录下生成hello.s文件。
3.3 Hello的编译结果解析
Hello.s为汇编程序语言文本文件,依然可以直接打开。
图3.2 汇编程序语言文本文件展示
3.3.1 伪指令解析
CSAPP书中第三章提到汇编文本中以’.’开头的部分是指导汇编器和链接器工作的伪指令,我们通常可以忽略这些行。但当我们学完第七章之后,也能对这一部分做一些解析。
首先.file声明了源文件名称与类型为名称是“hello”的.c文件,.text中存放已编译的机器代码,.section .rodata是只读数据段,.align则表明了数据对齐的方式为八位对齐,.string存放着格式字符串, .global为全局变量与函数,.type声明标识是函数/变量。
3.3.2 数据类型解析
(1) 字符串
图3.3 字符串
在3.3.1中就已分析,在开头的伪指令中便已经声明了字符串,前一个字符串是第一个输出内容“用法: Hello 学号 姓名 秒数!”,除了Hello外很容易看出汉字所用的是UTF-8编码,占用三位编码。
第二个字符串是输出格式字符串“Hello %s %s”,后面两个输出格式表明将输出两个字符串。
(2) 整型局部变量
图3.4 整型局部变量
对照源程序,可以看到定义了局部变量i,由汇编程序可以看出,i被保存在基指针%rbp-4的位置中,其中%rbp基指针即一开始的栈指针。
(3) 数组
图3.5 数组
程序中涉及到了argv这个数组。在机器代码中,首先通过-32(%rbp)为数组让出空间,不难看出argv[0~3]被分别存放在距离%rsp(%rbp)8, 16, 24的位置上,这也告诉我们数组赋值通过机器指令完成。
3.3.3 控制与数据操作
(1) 赋值操作
赋值操作需要利用mov语句,本程序中,是给局部变量i赋初值0,因此需要通过寄存器操作。
图3.6 赋值操作
由于i是int类型的变量,占据4个字节,因此通过movl进行复制,其中l代表双字。
值得一提的是,倘若i是全局或局部变量,将会根据是否赋初值存放在.data或.bss段中。
(2) 算术操作
图3.7 算术操作
本程序涉及到的算术操作主要是循环中每次i需要加1,由于i是int类型,这里利用addl来进行,每次给-4(%rbp)位置上的数值+1,就实现了i++的操作。
(3) 控制转移
(3.1) if语句
图3.8 if语句
这里是判断argc是否为4,即是否有4个参数,利用je和jmp跳转语句,倘若相等就跳转至.L2段继续执行程序,否则执行26-30行退出程序。
(3.2) for循环
图3.9 循环语句
通过CSAPP的学习,我们可以轻松发现这里是一个典型的循环结构,编译器所采用的方式是跳转到中间策略,每次符合条件就跳转到中间,否则跳出循环。
当i < 10时进行循环,每次循环i++。比较 i与9是否相等,在i<=9时继续循环,进入.L4,i>9时跳出循环。
3.3.4 数组/指针操作
这里在3.3.2的(3)中就已经分析,数组地址存储在栈中,可以利用栈指针的偏移量来读取.
3.3.5 函数调用操作
函数调用在C语言中需要经过如下操作:
- 传递控制:调用Q时,PC必须为Q的起始位置,同时要把返回位置压入栈中,以便正确返回到下一条语句。
- 传递数据:调用Q时,需要用正确的寄存器或栈帧传递参数,通常前六个参数所用的寄存器按顺序依次为%rdi 、%rsi、%rdx、%rcx、%r8、%r9。
- 分配和释放内存:开始Q前,Q需要为在Q中的一些局部变量分配空间,返回前还要释放空间。
(1)main主函数
图3.10 函数调用
通过寄存器%edi和%rsi存传入的参数argc和argv[],启动主函数的调用。
(2) exit函数
图3.11 exit函数
在调用exit之前,先通过寄存器%edi传入参数1。这是调用exit的方式。
(3) printf函数
3.12 printf函数
同样先在寄存器%rsi和%rsi中准备好要输出的参数argv[0]和argv[1],再调用printf输出。
(4) atoi函数
3.13 atoi函数
这里是把一个字符串通过atoi转成数字,首先也是把字符串传入%rdi中。
(5) sleep函数
3.14 sleep函数
这里将(4)中转换后的数字导入sleep函数中,让程序产生休眠效果。
3.4 本章小结
本章介绍了编译的概念及作用,通过对Hello.s实例的分析,从数据类型、过程调用等角度简述了编译后形成的汇编文本文件的结构及作用。
第4章 汇编
4.1 汇编的概念与作用
汇编器将.s文件翻译成机器语言指令,并把这些指令打包成可重定位目标程序,结果保存在.o的二进制文件当中。
4.2 在Ubuntu下汇编的命令
命令:gcc -c -o hello.o hello.s,其中-c是生成可重定位目标程序的指令。
图4.1 生成可重定位目标程序指令
执行后,目录下将出现.o文件,这是一个二进制文件。
4.3 可重定位目标elf格式
由于可重定位目标程序是一个二进制文件,我们不能直接打开它。可以通过readelf读出其各节的基本信息。具体命令是:readelf –a hello.o。
4.3.1 ELF文件头
图4.2 ELF头
ELF头以一个16字节的Magic序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。前4个字节包含一个magic number,第五个字节表示ELF的文件位数,“02”即表示为ELF64位,第六个字节“01”表示用小段法来表示数据。
ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
4.3.2 节头部表
图4.3 节头部表
节头部表描述了不同节的大小和位置。其中常用的有:.text是已编译程序的机器代码;.rela.text是一个.text节中位置的列表;.data中存放着已初始化的全局和静态变量;.bss中则存放着未初始化的全局和静态变量以及所有被初始化为0的全局和静态变量;.rodata为只读数据;.symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。该头部表共有14项。这张节头部表中,我们可以发现.data和.bss节的偏移竟然是一致的d8,这说明.bss在.o中还没有被分配空间,即未初始化的变量在.o不占空间,只是占位符。
4.3.3 重定位节
图4.4 重定位节
汇编器不知道数据和代码在内存中的具体位置,当遇到这些目标时就会生成一个重定位条目。.rela.text中保存的是.text节中需要通过重定位被修改的信息,.rela.eh_frame节是.eh_frame节重定位信息。
4.3.4 符号表
图4.5 符号表
.symtab是一个符号表,存放在程序中定义和引用的函数和全局变量的信息。其中value是距定义目标的节的起始位置的偏移,size是目标的大小。从这张符号表中可以看出,上文分析的exit、printf、atoi等都在其中。在这张符号表中,我们可以发现每个可装入节的起始位置都为0,说明还没有被映射到内存空间上。
4.4 Hello.o的结果解析
使用命令objdump -d -r hello.o,将得到的结果与Hello.s进行比较,发现如下差异:
- hello.o的头部没有了伪指令部分。
- 数值的表示
图4.6 数值的表示
可以看到Hello.o中变为了十六进制表示,这与上文分析中的十进制有所不同。
- 分支跳转
图4.7 分支跳转
不难发现在Hello.o中变为了相对寻址跳转,体现为目标指令地址与当前指令下一条指令的地址之差,而不是上文中的.L2 .L3通过段名称直接跳转。
- 函数调用
图4.8 函数调用
可以看出在Hello.o中变为了我们熟知的call相对寻址,call 的目标地址是当前指令的下一条指令,而Hello.s则采6用了如call atoi@PLT的方式。
4.5 本章小结
本章介绍了汇编的概念及作用,详细阐述了在Linux环境在汇编的操作过程,展示了可重定位目标文件的相关内容,研究了ELF文件的结构与内容,分析了汇编语言与机器语言之间的差异与联系。
第5章 链接
5.1 链接的概念与作用
链接指的是链接器合并被调用的标准库函数到预编译好的可重定位目标程序当中,要经过符号解析、重定位的步骤。
链接的作用是,它使分离编译成为了可能,这样程序员可以模块化编写程序,通过链接器静态或动态链接各个模块,降低开发大型软件的难度。
5.2 在Ubuntu下链接的命令
命令为:ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
图5.1 链接指令
执行后,目录下生成可执行文件Hello。
5.3 可执行目标文件hello的格式
命令:readelf -a hello
- ELF头
图5.2 ELF头
ELF头中的内容及含义与可重定位目标文件中基本一致,这里不再赘述。不同之处在于1.可执行文件的节数有27,可重定位目标文件则是14。2.程序头大小增加。3.能够从中活动入口地址。
2. 节头
图5.3 节头
节头部表描述了不同节的大小和位置。这里总共列出了27个条目,多于可重定位目标文件中的14个。此外,这里的每一个条目都有了自己的地址,这与Hello.o当中的全0是不同的。
- 程序头
图5.4 程序图
可执行文件的程序头表是一个结构数组,描述了偏移量、虚拟地址、物理地址等信息。程序头表是可重定位目标程序不具备的。
- 重定位节
图5.5 重定位节
重定位节的含义与可重定位目标程序中的基本一致。不同之处在于可执行程序中重定位节变为了.rela.dyn和.rela.plt。
- 符号表
图5.6 符号表
.symtab存放在程序中定义和引用的函数和全局变量的信息,与上一节不同,这里的条目数大大增加,名称也产生了变化。
- 版本信息
图5.7 符号信息
列出了所使用的版本信息。
5.4 hello的虚拟地址空间
使用edb加载hello,可以查看hello进程的虚拟地址空间各段信息。
图5.8 edb加载
可以看出虚拟地址起始于0x401000,结束于0x401ff0。结合这张虚拟内存地址表与5.3中的程序头表,可以知道各个节在内存中的位置。
例如.text节,在5.3的程序头表中,我们可以知道.text起始于0x4010f0,结束于0x401245。
图5.9 .text节
5.5 链接的重定位过程分析
命令:objdump -d -r hello
图5.10 反汇编
执行后可以获得反汇编代码。
比较与Hello.o的不同:
- 代码长度明显增加。直观上hello的代码长度明显增加,分节数增多。
- Hello中加入了所调用的库函数代码,例如exit、printf等。
图5.11 库函数代码
- 链接后反汇编代码中有部分节的内容,例如.init与.plt。
图5.12 节内容
- Hello中已经完成了链接,采用的是PC地址相对引用的方法。
5.13 相对地址应用
如上图所示,在划线处不难发现0x401160+0x4b=0x4x11ab,即用PC地址做相对跳转。
重定位过程分析:
- 重定位和符号定义。链接器将所有相同类型的节合并为同一类型的聚合节。
- 重定位节中的符号引用。链接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行的地址。
5.6 hello的执行流程
从edb调用Hello,可以看到其调用的程序依次如下:
ld-2.27.so!_dl_start-> ld-2.27.so!_dl_init->hello!_start->libc-2.27.so!__libc_start_main->-libc-2.27.so!__cxa_atexit->-libc-2.27.so!__libc_csu_init-> hello!_init-> libc-2.27.so!_setjmp->-libc-2.27.so!_sigsetjmp->--libc-2.27.so!__sigjmp_save->hello!main-> hello!puts@plt-> hello!exit@plt->*hello!printf@plt->*hello!sleep@plt->*hello!getchar@plt->ld-2.27.so!_dl_runtime_resolve_xsave->-ld-2.27.so!_dl_fixup->--ld-2.27.so!_dl_lookup_symbol_x-> libc-2.27.so!exit.
5.7 Hello的动态链接分析
动态链接指的是在程序运行或加载时,动态链接器将共享库加载到内存中并和程序链接起来。对于动态共享链接库中的PIC函数,编译器无法预测该函数的运行时地址,因此需要通过延迟绑定的方式。这通过GOT和PLT实现。
图5.14 动态链接分析
根据ELF信息可知在0x404000的位置上。
在EDB的Datadump中可以看到相关信息。以下是调用dl_init前的信息。
图5.15 调用dl_init前的信息
以下是调用dl_init后的信息。
图5.16 调用dl_init后的信息
可以发现0x404000段的内容发生了很大的变化,这就是把共享库的内容加载后的表现。
5.8 本章小结
本章介绍了链接的概念及作用,分析了可执行文件ELF头与可重定位目标文件ELF头的异同点,以及这两者反汇编信息的不同,加深了对于链接中重定位与符号解析的理解。
第6章 hello进程管理
6.1 进程的概念与作用
进程指的是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存中的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
作用:
- 它提供给应用程序一个独立的逻辑控制流,提供一个假象,好像我们的程序独占地使用处理器。
- 它提供给应用程序一个私有的地址空间,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell是一个交互型应用程序,用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行文件。
处理流程:
- 从终端读取输入的命令
- 处理输入内容,通过eval、parseline等步骤切分字符串获得命令行参数。
- 调用buildtin检查输入的命令行参数是否是一个内置的shell命令,如果是则立即执行。
- 如果不是内置的shell命令,则调用fork创建新的进程。
- 在子进程中使用execve执行指定程序。
- 程序运行时,shell要监视用户输入并响应,可以异步接收来自I/O设备的信号,并做出响应。
- 使用waitpid命令等待文件结束并回收。
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的运行的子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的一份副本,包括代码和数据段、堆、共享库与用户栈。父进程和新创建的子进程之间的最大的区别在于它们有不同的PID。
在本程序中,通过./hello 2021113634 何栩晟 2调用,解析后,由于命令并不是shell的内置命令,因此会调用fork()创建一个子进程。
6.4 Hello的execve过程
Execve函数在当前进程的上下文中加载并运行一个新程序。 Execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。
图6.1 用户栈的组织结构
Execve函数在当前进程的上下文中加载并运行一个新的程序,它会覆盖当前进程的地址空间,但并没有创建一个新的进程。
在execve加载了filename之后,它调用7.9节中描述的启动代码。启动代码设置栈,并将控制传递给新程序的主函数。Execve在调用成功时不会返回,只有在出现错误时才会返回。
6.5 Hello的进程执行
6.5.1 上下文
内核为每个进程维持一个上下文,上下文是内核重新启动一个被挂起的进程所需的状态。上下文由一些对象的值(是这些对象的值而非对象本身)组成,这些对象包括:通用目的寄存器、浮点寄存器、状态寄存器、程序计数器、用户栈、内核栈和各种内核数据结构。
内核使用上下文切换来调度进程,具体步骤有:
- 保存当前进程的上下文
- 恢复某个先前被抢占的进程被保存的上下文
- 将控制传递给这个新恢复的进程
图6.2 进程上下文切换的剖析
6.5.2 进程时间片
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
6.5.3 逻辑控制流
使用调试器单步执行程序时会看到一系列的程序计数器(PC)值,这个 PC 的值的序列叫做逻辑控制流,简称逻辑流。PC 的值唯一地对应于包含在程序的可执行目标文件中的指令,或包含在运行时动态链接到程序的共享对象中的指令。
图6.3 逻辑控制流
6.5.4 内核模式和用户模式
处理器使用某个控制寄存器中的一个模式位来区分用户模式与内核模式。进程初始时运行在用户模式,当设置了模式位时,进程就运行在内核模式。运行在内核模式的进程可以执行指令集中的任何指令,并可以访问系统中的任何内存位置。运行在用户模式的进程不允许执行特权指令,比如停止处理器、改变模式位、发起 I/O 操作等,也不能直接引用地址空间内核区中的代码和数据,用户程序只能通过系统调用接口间接地访问内核代码和数据。
进程从用户模式变为内核模式的方法是通过中断、故障、陷阱(系统调用就是陷阱)这样的异常。异常发生时,控制传递给异常处理程序,处理器将模式从用户模式转变为内核模式。
6.5.5 Hello进程的执行过程
调用execve后,Hello已经被分配了虚拟地址空间。Hello起始阶段在用户模式下运行,按照程序内容正常输出Hello 2021113634 何栩晟,之后调用sleep,通过陷进进入内核模式,内核处理sleep指令进入休眠状态,并切换上下文让其它进程先行,计时器计时结束后又会发出一个中断信号,内核模式被中断,重新开始执行hello进程。运行过程由于要多次执行sleep,因此会不断切换上下文,使进程调度的效率达到最高。
6.6 hello的异常与信号处理
6.6.1 程序正常的运行状态
图6.4 程序正常运行状态
输入指令./hello 2021113634 Hexusheng 1,程序如期输出九条语句,并在每条指令输出后暂停1s。
6.6.2 ctrl + Z
图6.5 发送SIGSTP信号
在程序正在运行时使用ctrl + Z,内核将会发送一个SIGSTP给这个进程组中的每一个进程,Hello被挂起,调用ps仍能看到该任务。
图6.6 ps查看进程
使用fg可以重启Hello进程。
6.6.3 ctrl + C
运行过程中调用Ctrl+C,内核会发送SIGINT信号给该进程组的每个进程,此时Hello会被直接终止,ps讲看不到此进程。
图6.7 发送SIGINT信号
6.6.4 乱按(不回车)
由于读入采用的是getchar(),因此在键盘上乱按(不回车)会使输入的字符实时显示,但没有额外影响。
6.6.5 回车
输入回车后,shell会接受指令,并把输入内容进入缓冲区,待程序运行结束后逐个处理。
6.6.6 进程树与kill
用pstree可以展示进程树。
图6.8 进程树展示
用kill杀死进程。正的 PID 表示发送到对应进程,负的 PID 表示发送到对应进程组中的每个进程。-9表示发送信号9,即SIGKILL。
图6.9 发送SIGKILL信号
6.7本章小结
本章介绍了进程的概念和作用,通过对hello的逐步分析,分析fork等函数的原理,通过Ctrl+C和Ctrl+Z发送信号,研究对于异常以及信号的处理结果。
第7章 hello的存储管理
注:第7章、第8章为未要求内容,这里根据自己自学内容,挑选部分章节撰写。
7.1 hello的存储器地址空间
分为逻辑地址、线性地址、虚拟地址、物理地址。
1. 逻辑地址:逻辑地址是指由程序产生的与段相关的偏移地址部分,逻辑地址由选择符和偏移量两部分组成。即hello.o中的相对偏移地址。
2. 线性地址:逻辑地址经过段机制后转化为线性地址,其地址空间是一个非负整数地址的有序集合。具体以hello而言,线性地址标志着 hello 应在内存上哪些具体数据块上运行。
3. 虚拟地址:虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。仍为hello里面的虚拟内存地址。
4. 物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。就是hello在运行时虚拟内存地址对应的物理地址。
7.6 hello进程fork时的内存映射
执行命令./hello 2021113634 Hexusheng时,shell发现./hello并不是内置命令,便会调用fork,内核会为hello创建各种数据结构,为它分配一个独立的PID。它同时创建了mm_struct、区域结构和页表的原样副本,给它创建虚拟内存。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在shell中加载并运行包含在可执行目标文件hello中的程序,加载并运行hello需要以下几个步骤:
1. 删除已存在的用户区域。删除当前进程hello虚拟地址的用户部分中已存在的区域。
2. 映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。.bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。
3. 映射共享区域。hello程序与共享对象或目标(如标准C库libc.so)链接时,将这些对象动态链接到hello程序,然后再映射到用户虚拟地址空间中的共享区域内。
4. 设置程序计数器(PC)。execve设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
文件是Linux管理的基本思想,在Linux中,所有的IO设备都会被模型化为文件,而所有的输入和输出都会被转化为对文件的操作。这使得所有的输入和输出都能以一种统一且一致的方式来执行:打开文件、改变当前的文件位置、读写文件、关闭文件。
8.2 简述Unix IO接口及其函数
(一)Unix IO接口
1. 打开文件
一个应用程序通过要求内核打开相应的文件来表示它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符。对于Shell创建的每个进程,其都有三个打开的文件:标准输入,标准输出,标准错误。
2. 改变当前的文件位置
应用程序通过seek,能够将内核中保存的当前文件位置k进行修改,k表示从文件开头起始的字节偏移量。
3. 读写文件
读时,假设读取n字节,即从k增加到k+n;写时,从内存中复制n个字节到一个文件,从当前文件位置k开始,更新k。
4. 关闭文件
内核释放打开文件时创建的数据结构,恢复描述符至可用描述符池中。
(二)Unix IO函数
1. int open(char *filename, int flags, mode_t mode); //若成功则返回文件描述符,若出错返回 -1
2. int close(int fd); // fd 是一个文件描述符
3. ssize_t read(int fd, void *buf, size_t n); //若成功则返回读的字节数,若 EOF 则返回 0,若出错返回 -1
4. ssize_t write(int fd, const void *buf, size_t n); //若成功返回写的字节数,若出错返回 -1
5. int stat(const char *filename, struct stat *buf);
6. int fstat(int fd, struct stat *buf);
7. lseek(); //更改当前文件位置。
结论
Hello的一生经历了如下过程:
1. 预处理
将程序中所调用的头文件以文本形式插入到程序的文本当中,将hello.c预处理为hello.i。
2. 编译
通过语法、词法分析将指令翻译成汇编语言文本文件,编译器将hello.s编译为hello.s。
3. 汇编
将汇编语言程序翻译成机器语言指令,生成可重定位目标文件hello.o。
4. 运行
在shell中输入./hello 2021113634 Hexusheng,中断在识别./hello不是自带指令后,将会通过fork为其创建新进程,通过execve把代码数据加载入内存。
5. 信号处理
进程可以被信号处理,通常可以通过键盘发送特定信号,shell会调用对应的信号处理程序对信号进行终止、挂起等操作。
6. 存储管理、虚拟内存管理
7. 终止并等待回收
Shell等待回收hello子进程,内核删除有关hello的所有信息。
Hello的一生很短,短到仅仅只能出现在屏幕上数十秒,尽力展现自己的内容;但Hello的一生又很长,长到预处理、编译、汇编……缺少任何一步,它都不能走到最后,只能获得短短的几条报错信息。这何尝不像处于计算机学习之路上的我们,台上一分钟,台下十年功,最终精湛的技艺需要通过一门门如CSAPP的课程打下坚实的基础,计算机专业的同学们,加油吧!
附件
文件名 | 功能 |
ello.i | 预处理后得到的文本文件 |
hello.s | 编译后得到的汇编语言文本文件 |
hello.o | 汇编后得到的可重定位目标文件 |
hello.elf | 用readelf读取hello.o得到的ELF格式信息 |
hello | Hello的可执行文件 |
helloexc.asm | 反汇编hello.o得到的反汇编文件 |
helloexc.elf | 由hello可执行文件生成的.elf文件 |
helloexc.asm | 反汇编hello可执行文件得到的反汇编文件 |
参考文献
[1] Randal E.Bryant, David O'Hallaron. 深入理解计算机系统[M]. 机械工业出版社.2018.4
[2] http://docs.huihoo.com/c/linux-c-programming/ C汇编Linux手册