程序人生-Hello’s P2P
P2P (Program to Process):名为hello.c的Source program被GCC编译器的驱动程序读取并翻译为可执行目标文件hello,而后,操作系统外壳shell利用函数fork为hello创建进程process,可在其中加载程序。自此,hello的P2P过程已经完成,hello由一个Program变为Process。
如图1所示,GCC的翻译过程分为预处理,编译,汇编,链接四个阶段,预处理器(cpp)根据以字符 # 起始的命令扩充并修改源文件hello.c,得到被修改的源文件hello.i,其经过编译器(ccl)处理被翻译成汇编语言程序hello.s,随后汇编器(as)将hello.s翻译成由机器语言指令打包生成的可重定位目标程序hello.o,其与printf.o文件由链接器(ld)合并为可执行目标文件hello,可被加载到内存中由系统执行。
图1
O2O (Zero-0 to Zero-0):shell调用execve函数在创建子进程中加载程序hello。该过程首先删除当前虚拟地址的用户部分已存在的数据结构,为hello的代码、数据、bss和栈区域创建新的区域结构,而后映射共享区域并设置程序计数器指向代码区域入口,CPU在流水线上执行指令。程序运行结束时,shell接收到相应信号,回收hello进程,内核删除相关数据并释放内存。自此,hello的O2O过程已经完成,hello由0到有,最终归为0。
1.2环境与工具硬件环境:X64 CPU;2.30GHz;16.0GB RAM;1TD
软件环境:windows11 64位;VMware 17.0.2;Ubuntu 20.04 LTS 64位
开发工具:Visual Studio 2022 64位;gdb;edb;vi/vim;gcc
1.3 中间结果本章介绍了 hello 的 P2P 和 O2O 过程,描述了实验环境与开发工具,并列出生成的中间结果文件名称及其作用。
第2章 预处理
2.1 预处理的概念与作用预处理的概念:
预处理器(cpp)在编译源程序文件之前,由预处理程序对程序源文件进行文本方面的操作,如文本替换、文件包含、删除代码等,生成被修改的源程序文件。
预处理的作用:
预处理根据以字符 # 开头的命令,修改原始的C程序。预处理指令主要分为以下五类:
1)定义声明
使用 ****#****define 命令定义一个标识符(宏名)和一个串(字符集),每当预处理程序在源程序中发现标识符时,都用相应串替换标识符,该替换过程称为宏代换。
一般形式如下:
****#****define MACRO_NAME(args) tokens(opt)
2)条件编译
编译指令****#****if, ****#****elif, ****#****else, ****#****endif等允许程序员有选择的编译程序源代码的不同部分,预处理程序根据条件有选择性的保留或者放弃源文件中的内容。
一般形式如下:
****#****if 常量表达式
语句…
****#****elif 常量表达式
…
****#****elif 常量表达式
…
…
****#****else
…
…
****#****endif
3)文件包含
****#****include 指令用于文件包含,要求编译程序读入一个源文件。预处理程序搜索指定文件并将其内容放在当前所在位置。
一般形式如下:
****#****include “headfile”
****#****include
****#****include 预处理标记
4)预定义宏
标准C中定义了一些对象宏, 这些宏的名称以 __ 开头和结尾,并且都是大写字符,可以被 ****#****undef,也可以被重定义。
5)扩展控制
****#****line指令用来修改预定义宏__LINE__和__FILE__的内容,其中预定义宏 __LINE__ 的内容是当前被编译代码行的行号,__FILE__的内容是当前编译源文件的文件名。
一般形式如下:
****#****line number “filename”
其中number是被赋予__LINE__ 的正整数,filename是被赋予 __FILE__的合法文件标识符。
2.2在Ubuntu下预处理的命令linux下使用gcc预处理的命令为:
gcc -E hello.c -o hello.i
截图1
2.3 Hello的预处理结果解析如截图2所示,预处理得到的文本文件hello.i被扩充至3092行,起始代码段是hello.c拼接的各种库文件,中间代码段对内部函数进行声明,而后是stdio.h unistd.h stdlib.h的源代码展开,源程序的代码在文件末尾。
截图2
2.4 本章小结本章主要介绍预处理的概念与作用,并对hello的预处理结果进行解析,分析了被修改的源文件hello.i的结构组成。预处理过程与其说是扩充文本,不如说是补全了文本内容,使其最终能够运行在操作系统的上下文中。
第3章 编译
3.1 编译的概念与作用编译的概念:
编译器(ccl)将预处理生成的文本文件,翻译成汇编语言程序的文本文件。汇编语言程序的每条语句都以一种固定文本格式描述一条低级机器语言指令,为不同高级语言的不同编译器提供了通用的输出语言。
编译的作用:
在编译过程中,编译器将将源代码输入扫描器,将源代码的字符序列分割成一系列记号并生成生成语法树,再由语义分析器判断语句是否合法。编译过程中对程序进行多种等价变换,以生成更有效的目标代码。最后由目标代码生成器将语法分析或优化后的中间代码变换为汇编语言代码。
3.2 在Ubuntu下编译的命令gcc -S hello.i -o hello.s
截图3
3.3 Hello的编译结果解析 3.3.1 数据1)立即数常量
如截图4所示,hello.c中出现的数字被视为立即数常量,以 $立即数 的形式直接存放在汇编代码中。
截图4
2)格式串
如截图5所示,printf 函数打印的格式串包括格式控制字符串和转义字符,是一种特殊的字符串常量,以UTF-8编码形式存放在.rodata节的段.LC0与.LC1中,访问时通过rIP + 偏移量间接寻址。
截图5
3)局部变量
如截图6所示,hello.c中的局部变量在程序运行时储存在函数调用栈中,访问时通过rbp + 偏移量间接寻址。
截图6
3.3.2 赋值hello.c中的赋值操作主要通过MOV类指令实现,其形式如图2所示:
图2
如截图7所示,程序中使用指令movl将0赋予局部变量i。
截图7
3.3.3 算术操作如截图8所示,hello.c中涉及算术操作i++,该操作使用addl指令实现。
截图8
图3列出了x86-64的一些算术和逻辑操作:
图3
3.3.4 关系操作汇编语言的关系操作主要通过比较测试指令实现,指令如图4所示:
图4
如截图9所示,hello.c中使用cmpl实现对argc,i的比较。
截图9
3.3.5 数组操作main函数形式如下:
int main(int argc, char *argv[ ])
函数参数中含有指针数组argv[ ],数组单元存放指向输入字符串的指针,参数argc为输入字符串个数。
如截图10所示,程序向printf传送参数argv[1]与argv[2],向atoi函数传送参数argv[3],访问时采用地址********+********偏移量的方式定位数组中元素,可知存放在寄存器rsi的参数argv[ ]首地址存放在%rbp - 32,存放在寄存器rdi的参数argc存放在%rbp - 20,通过%rbp - 32存放的argv[ ]首地址加偏移量访问argv[1]、argv[2]、argv[3]。
截图10
3.3.6 控制转移hello.c中if条件与for循环语句涉及控制转移,程序通过cmpl与je、jle指令实现比较与跳转,如截图11所示:
截图11
汇编语言通过跳转指令jump与CPU存放的条件码实现控制转移,jump指令如图5所示:
图5
3.3.7 函数操作汇编语言使用call指令调用函数,ret指令返回, 函数参数保存在寄存器和栈中,规则如图6所示:
图6
如截图12,hello.c依次调用了printf、atoi、sleep等函数,将参数argv[1],argv[2]分别放入寄存器rsi,rdx中,将参数argv[3]放入寄存器rdi中,将函数位于寄存器rax的函数atoi返回值作为函数sleep的参数放入寄存器rdi中。
截图12
3.4 本章小结本章围绕hello.i编译生成的汇编程序文本hello.s进行分析,通过对其中汇编指令的解读,探究汇编器将C语言中各个数据以及操作转换为汇编语言指令,为不同高级语言的不同编译器之间搭建了沟通的桥梁,以便最终生成机器可执行的二进制指令代码。
第4章 汇编
4.1 汇编的概念与作用汇编的概念:
汇编器(as)将汇编语言指令翻译成机器语言指令,并将指令打包生成一种可重定位目标程序的格式,存储在二进制目标文件的过程。
汇编的作用:
将文本形式的汇编语言指令代码转换为对应机器可执行的二进制指令。
4.2 在Ubuntu下汇编的命令gcc -m64 -no-pie -fno-PIC -c -o hello.o hello.s
截图13
4.3 可重定位目标elf格式 4.3.1 ELF头x86-64 linux系统中使用可执行可链接格式(ELF)存储可重定位目标程序,典型的ELF可重定位目标文件格式如图7所示:
图7
使用readelf -h hello.o命令查看ELF头,如截图14:
截图14
EFL头以16字节的Magic序列开始,该序列描述了生成文件的系统字节大小和字节顺序,其余部分包含ELF头的大小、目标文件类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量等信息,帮助链接器语法分析和解释目标文件。
4.3.2 节头部表使用readelf -S hello.o命令查看节头部表,如截图15:
截图15
节头部表描述可重定位目标文件中各个节信息,包括节的名称、类型、地址及其偏移量、大小等信息。
4.3.3 符号表使用readelf -s hello.o命令查看符号表,如截图16:
截图16
符号表依次记录了程序中出现的各种符号及其重定位值、大小、类型、是否为全局符号、是否可见、位置、名称等信息。在重定位前初始化value值均为0,Ndx为UND表明符号需经过链接从外部获取定义。
4.3.4 重定位条目使用readelf -r hello.o命令查看符号表,如截图17:
截图17
汇编器遇到对位置未知的目标引用时,会生成一个重定位条目,包含有符号的偏移量、基址信息、类型、符号名称等,以便链接器后续将目标文件合并成可执行文件时修改引用。
4.4 Hello.o的结果解析使用objdump -d -r hello.o命令反汇编hello.o,与hello.s进行对照分析,发现以下不同点:
1)进制转换
如截图18所示,hello.s文件中的十进制操作数在反汇编后表示为hello.o.s文件中的十六进制操作数。
截图18
2)控制转移
如截图19所示,hello.s中控制转移的目标位置.L1、.L2等助记符,在反汇编后表示为hello.o.s文件中的指令偏移地址。
截图19
3)函数调用
如截图19所示,hello.s文件中函数调用直接引用函数的名称,而在反汇编后生成的hello.o.s文件中,函数调用地址是下一条程序的地址。在机器语言中,不确定地址的函数调用时,指令后的相对地址初始化为0,等待链接器的进一步处理。
4.5 本章小结本章介绍了hello.s汇编生成hello.o的过程,通过readelf工具查看可重定位目标文件hello.o的ELF内容,使用反汇编工具objdump得到反汇编文件hello.o.s并与hello.s进行比较,探究汇编的具体作用。
第5章 链接
5.1 链接的概念与作用链接的概念:
链接器(ld)将各文件的代码与数据综合,通过符号解析和重定位等过程,最终生成一个可以在程序中加载并运行的可执行目标文件的过程。
链接的作用:
链接过程将多个可重定位目标文件合并以生成可执行目标文件,令分离编译成为可能。在程序修改后无需重新编译整个工程,而是仅编译修改的文件,极大的方便了大型应用程序的修改和编译。链接还有利于构建共享库,可将常用函数(如printf函数)储存在常用函数文件中,等待链接时与源程序进行合并生成可执行文件,极大的节省空间。
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
截图20
5.3 可执行目标文件hello的格式 5.3.1 ELF头如截图21,使用readelf -h hello命令查看hello的ELF头,并与hello.o的ELF头比较,发现程序类型由可重定位文件REL变为可执行文件EXEC (Executable file),入口点地址由0变为0x4010f0,ELF头的其余参数信息也被填充完整。
截图21
5.3.2 节头部表如截图22,使用readelf -S hello命令查看hello的节头部表,并与hello.o的节头部表比较,发现链接器将各个文件对应段合并,且重新分配计算了相应节的类型、位置、大小等信息,各个节的地址也从 0 开始进行分配。可以看到.text节起始地址为 0x4010f0,符合ELF头中的程序入口地址。
截图22
5.3.3 符号表如截图22,使用readelf -s hello命令查看hello的符号表,并与hello.o的符号表比较,发现符号表的内容在合并后有所扩充。
截图22
5.3.4 重定位条目如截图23,使用readelf -r hello命令查看hello的重定位条目,并与hello.o的重定位条目比较,发现重定位条目的内容在链接后有较大改变。
截图23
5.3.5 程序头
截图24
5.4 hello的虚拟地址空间使用edb打开hello可以查看hello的虚拟地址空间,如截图25所示,hello的起始于虚拟地址0x401000,与hello的ELF文件中init的虚拟地址相同,类似的,可看到hello各节的起始地址与ELF文件中的虚拟地址一一对应。
截图25
5.5 链接的重定位过程分析使用objdump -d -r hello命令反汇编hello,与hello.o反汇编生成的文件hello.o.s进行对照分析,发现以下不同点:
1)新增函数
如截图26所示,hello的反汇编文件中在链接后新增了许多库函数,如puts,printf,getchar,atoi,exit,sleep等。
截图26
2)新增节
如截图27所示,hello的反汇编文件中在链接后新增了.init节和.plt节
截图27
3)地址变化
如截图28所示,hello的反汇编文件中函数地址表示由hello.o.s中的相对地址变为虚拟地址。
截图28
重定位:链接器(ld)在完成符号解析以后,将代码中的符号引用与对应符号定义建立关联,同时获得目标文件中代码节和数据节的大小信息。随后,链接器合并输入模块,并为每个符号分配运行时地址,即重定位过程。
重定位分为两步:首先进行重定位节和符号定义,链接器将输入模块中相同类型的节合并,为程序中的每条指令和全局变量赋予唯一的运行时地址。随后进行重定位符号引用,链接器根据重定位条目中存储的节偏移量与修正值,结合起始地址,计算出引用符号的运行时地址并更新符号引用。
5.6 hello的执行流程
截图29
从加载hello到_start,到call main,至程序终止的所有过程,调用与跳转的各个子程序如下:
<ld-2.31.so!_dl_start>
<ld-2.31.so!_dl_init>
<hello!_start>
<libc-2.31.so!__libc_start_main>
<hello!main>
<libc-2.31.so! printf >
<libc-2.31.so! atoi >
<libc-2.31.so! strtoq >
<libc-2.31.so! sleep >
<libc-2.31.so! nanosleep >
<libc-2.31.so! clock_nanosleep >
<libc-2.31.so! _IO_file_xsputn>
<libc-2.31.so!getchar>
5.7 Hello的动态链接分析在进行动态链接前,首先进行静态链接,生成部分链接的可执行目标文件 hello。此时共享库中的代码和数据没有被合并到hello中,而是在加载 hello 时,动态链接器对共享目标文件中相应模块内的代码和数据进行重定位,加载共享库并生成完全链接的可执行目标文件,hello中的printf、sleep、atoi等都通过动态链接与源程序建立关系。
由于调用共享库函数时,无法预测函数的运行时地址,因此要为该引用生成一条重定位记录,由动态链接器在程序加载时解析。linux使用延迟绑定,将过程地址的绑定推迟到第一次调用该过程时。
延迟绑定通过数组GOT和PLT实现:数组PLT中每个条目大小为16字节,PLT[0]是跳转到动态链接器中的特殊条目,每个条目负责调用一个具体函数;数组GOT中每个条目存放8字节大小的地址,GOT[O]和GOT[1]包含动态链接器在解析函数地址时使用信息,GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,地址在运行时被解析。
5.8 本章小结本章解释链接的概念及作用,通过分析hello的ELF格式,虚拟地址空间,重定位过程,执行流程,和动态连接过程,探究可重定位文件hello.o链接生成可执行文件hello的各个过程。
第6章 hello进程管理
6.1 进程的概念与作用进程的概念:
进程是一个执行中程序的实例,在现代系统上运行一个程序时,会得到一个假象:我们的程序好像是系统中当前运行的唯一程序,独占使用处理器和内存;处理器好像是无间断的执行我们程序中的指令;最后,我们程序的代码和数据好像是系统内存中唯一的对象。
进程的作用:
进程提供给应用程序两个关键假象:一个独立的逻辑控制流、一个私有的地址空间。没有进程,如此庞大的计算机系统不可能设计出来。
6.2 简述壳Shell-bash的作用与处理流程Shell-bash的作用:
Shell-bash是交互型的应用程序,为用户与操作系统内核之间搭建桥梁,负责进程创建、程序加载运行、前后台控制、作业调用、信号发送与管理等工作。
Shell-bash的处理流程(以运行可执行文件hello为例):
1)在shell命令行中输入命令:./hello
2)shell命令行解释器构造argv和envp
3)调用fork()函数创建子进程,将hello中的.text节、.data节、.bss节等内容加载到当前进程的虚拟地址空间中
4)调用execve()函数跳转到程序入口点,开始执行_start函数,hello开始执行
6.3 Hello的fork进程创建过程shell调用fork函数创建子进程,子进程得到与父进程用户级虚拟地址空间相同但独立的一份副本,包括代码和数据段、堆、共享库以及用户栈,同时获得与父进程打开文件描述符相同的副本,但与父进程拥有着不同且唯一的PID。
6.4 Hello的execve过程子进程调用execve函数在当前进程的上下文中加载并运行程序hello,execve调用内存中的加载器执行hello程序,execve函数拥有三个参数,包括可执行目标文件名filename、参数列表argv和环境变量列表envp。加载器删除子进程现有的虚拟内存,并创建一组新的代码、数据、堆和栈段,其中栈和堆被初始化为零。通过内存映射将代码和数据段初始化为可执行文件中的内容。最后加载器设置PC指向_start地址,以调用hello中的main函数。在加载过程中没有任何数据从磁盘复制到内存,直到CPU引用一个被映射的虚拟页时才会进行复制,图8展示了新程序开始时用户栈的典型结构。
图8
6.5 Hello的进程执行为节省时间,系统会运行多个进程,进程间轮流使用处理器。内核为每个进程维持一个上下文,存有内核重新启动一个被抢占的进程所需要的状态,如寄存器、程序计数器、用户栈等。为提供一个无懈可击的进程抽象,处理器须限制应用可执行指令及可访问地址空间的范围。处理器使用某寄存器的一个模式位设定运行模式,以圈定处理器访问范围。
执行hello进程时,由于从磁盘中读取数据的时间较长,会通过陷阱异常切换到内核模式。此时系统进行进程调度,重新开始一个先前被抢占了的进程。切换进程运行了足够长时间后,系统再次进行进程调用,抢占执行进程hello,hello由内核态切换为用户态。
6.6 hello的异常与信号处理如图9所示,异常可分为中断、陷阱、故障、终止四类。
图9
图10展示了linux信号的种类:
图10
通过调试,可在hello程序运行过程时对于异常与信号的处理:
1)乱按键盘
如截图30所示,在程序运行时乱按键盘,触发中断,hello进程并没有接收到信号,乱序输入的字符串被认为是命令,缓存在stdin中。
截图30
2)按下Ctrl + Z
如截图31、32、33所示,在程序运行时按下Ctrl + Z,hello进程接收到SIGSTP信号被挂起。使用命令ps查看后台进程,发现hello的PID是11064;再使用命令jobs查看当前作业,此时hello的job号为1;输入命令pstree查看进程树;输入fg向进程发送信号SIGCONT将其调回前台继续运行;最后输入kill向进程发送SIGKILL信号,终止hello进程。
截图31
截图32
截图33
3)按下Ctrl + C
最后除了简历做到位,面试题也必不可少,整理了些题目,前面有117道汇总的面试到的题目,后面包括了HTML、CSS、JS、ES6、vue、微信小程序、项目类问题、笔试编程类题等专题。
Ongwu博客 版权声明:以上内容未经允许不得转载!授权事宜或对内容有异议或投诉,请联系站长,将尽快回复您,谢谢合作!