择要
Hello是程序员的“初恋”,是每个程序员的起点。而仅仅实现打印出hello,对于一个合格的程序员来说,这往往是不够的。此次将从更为底层的角度入手,来相识Hello的一生。
本文将展示Linux系统,x86-64架构下的hello程序P2P和O2O的过程,从编译、进程管理和内存管理三个方面来详细阐述,以此来展现计算机系统的重要工作机制。
关键词:Linux;预处理;编译;汇编;链接;进程管理;内存管理
第1章 概述
1.1 Hello简介
Hello的P2P :From Program to Process,指Hello从一个程序转变到一个进程的过程。此中,Hello.c颠末编译器gcc的预处理、编译、汇编、链接后,酿成一个可实行文件Hello.out。进一步,在shell中实行Hello.out,shell会调用fork()函数为其生成一个子进程,然后调用execve()函数加载该程序,最后完成打印“Hello”。
Hello的O2O:From Zero-0 to Zero-0,指Hello从最开始什么都没有,到颠末编辑器编写生成.c文件,再颠末上述P2P过程,生成.out文件,从磁盘中加载到内存,再到最后实行完毕,被父进程接纳,由内核删除子进程的所有信息,包括分配的空间,一切又酿成0。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;2.80GHz;16.0G RAM;476G HD Disk;
1.2.2 软件环境
Windows10 64位;Vmware16;Ubuntu22.10 64位;
1.2.3 开辟工具
Visual Studio Code 1.76.1;CodeBlocks 64位;vi/vim/gredit+gcc;
1.3 中央结果
文件名说明hello.i由预处理器生成的文件hello.s由编译器生成的文件hello.o由汇编器生成的文件hello.out由链接器生成的文件o_ans.txtHell.o反汇编生成的文件out_ans.txtHello.out反汇编生成的文件 1.4 本章小结
本章简单描述了Hello的一生,从被编写,到编译,再到加载运行,再到最后被接纳,重新归0的过程。也列出了此次实验的环境,另有实验过程中生成的中央文件。
第2章 预处理
2.1 预处理的概念与作用
预处理就是预处理器(cpp)将预处理指令(以字符#开头的命令)转换为实际代码中的内容,从而生成.i文件。
作用:cpp扩展源代码,插入所有效#include命令指定的文件,并扩展所有效#define声明指定的宏。
2.2在Ubuntu下预处理的命令
指令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果剖析
可以看到在Hello.i文件中,相比于Hello.c文件,增长了好多代码段。全是从头文件stdio.h、unistd.h、stdlib.h中插入的,包含了许多外部变量,结构体,枚举等等。
2.4 本章小结
本章先容了编译系统中的预处理阶段,包括什么是预处理,即预处理有什么作用。还先容了在Linux系统下如何对源文件进行预处理操作。
第3章 编译
3.1 编译的概念与作用
编译指由编译器(ccl)对预处理完的文件hello.i进行一系列的词法分析、语法分析、语义分析和优化,翻译成文件hello.s,它包含一个汇编语言程序。
编译的作用:生成汇编语言,以便后续汇编器的转换。
(注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序)
3.2 在Ubuntu下编译的命令
指令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果剖析
3.3.1 数据范例
- 常量
好比源文件中的常量4,编译器ccl用立即数(前面添加$)进行表现;另有字符串,如”用法: Hello 学号 姓名 秒数!”,则对应于.rodata的.string中。
- 局部变量
对于主函数main的两个形式参数argc和argv,最开始存储在寄存器%edi和%rsi,厥后,便转移到main所分配的栈帧中。对于主函数中的int范例的变量i,也是存储在main函数的栈帧中-4(%rbp)。
3.3.2 赋值
编译器ccl利用mov指令给局部变量i赋初值0
3.3.3 范例转换
通过调用函数atoi()将字符串转换为int整数。
3.3.3 算术操作
编译器ccl利用add指令对局部变量i进行加1操作,即M(-4+%rbp)++
3.3.4 关系操作
编译器利用cmpl对argc和4进行比较,设置相应的条件码。如果相等(即零标志ZF=1),则跳转到.L2。
编译器ccl对i和7进行比较,并设置相应的条件码。如果i <= 7(即(SF^OF) | ZF=1),则跳转到.L2。
3.3.5 数组/指针操作
实际上就是通过栈指针%rsp加上字节偏移来对argv数组进行访问。
3.3.6 控制转移
实际上就和刚才讲过的关系操作时同等的。当满足argc==4时,便会发生跳转。
2) for循环
.L2是对i进行初始化。
.L4是循环体,里面是输出printf,另有调用函数sleep。
.L3是循环条件的判断,当i<=7时,则跳转到.L4实行循环体内语句,否则,实行.L3后续指令。
3.3.7 函数操作
编译器ccl利用call对函数进行调用。该程序中调用了exit()、printf()、sleep()、atoi()和getchar()5个函数。
3.4 本章小结
本章先容了编译的概念和作用,以及如何在Linux系统下对.i文件进行编译。还对编译后的.s文件中的数据和操作进行了详细的分析。
第4章 汇编
4.1 汇编的概念与作用
汇编指将hello.s翻译成呆板语言指令,并把这些指令打包成一种叫做可重定位目的程序的格式,并将结果保存在目的文件hello.o中。
作用:将汇编语言转换成呆板语言。
(注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成呆板语言二进制程序的过程。)
4.2 在Ubuntu下汇编的命令
指令:gcc -c hello.s -o hello.o(注意:-c是小写的c!)
4.3 可重定位目的elf格式
4.3.1 ELF头
从ELF头中可以获取该文件的根本信息,包括文件范例、呆板范例、以及节头部表的巨细等等。
4.3.2 节头部表(Section Headers)
节头部表界说了文件中的所有sections,包括其巨细、范例、地点和偏移。
4.3.3 符号表
符号表则是存放程序中界说和引用的函数和全局变量的信息。从Bind字段便可以分辨出该符号是全局符号照旧当地符号(部分还会显示为弱符号)。
4.3.4 重定位节
重定位节放置的是重定位条目。此中,offset是节内偏移,symbol表现所绑定的符号,Type则是重定位范例。
4.4 Hello.o的结果剖析
通过指令objdump -d -r hello.o对hello.o进行反汇编。与第3章的 hello.s进行对照分析,发现,最大的差别就是反汇编后的文件多了呆板代码。每条汇编语句都对应着一条呆板代码。而两者的区别如下:
- 反汇编后的呆板代码用十六进制表现操作数,而且字节次序也存在差异;而.s文件中的则是利用十进制来表现立即数。
- 反汇编后的指令的操作码并没有指明详细的字节数(即没有后缀b、w、l、q);而.s文件中都详细指明了处理的数据长度
- 对于分支转移,反汇编后的跳转指令则是利用详细地点,反面跟着的则是相对于主函数的偏移,例:jmp 86<main+0x86>;而在.s文件中,则是利用助记符来进行跳转,例:jmp .L3。
- 对于函数调用,反汇编后的call指令的操作数则是跳转的详细地点,例:call 68<main+0x68>;而在.s文件中,则是直接利用函数名作为跳转目的,例:call printf@PLT。
4.5 本章小结
本章先容了汇编的概念和作用,以及如何在Linux系统下进行汇编,并对可重定位目的文件的ELF格式进行分析,还对反汇编后的文件与原先的hello.s文件进行比较。
第5章 链接
5.1 链接的概念与作用
链接指将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并实行。
(注意:这儿的链接是指从 hello.o 到hello生成过程。)
5.2 在Ubuntu下链接的命令
指令如下:
ld -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 /usr/lib/gcc/x86_64-linux-gnu/12/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/12/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello.out
(此中,数字12为gcc的版本号)
5.3 可实行目的文件hello的格式
5.3.1 ELF头
5.3.2 节头部表(Section Headers)
5.3.3 程序头部表(Program Headers)
程序头部表用来表现可实行文件的连续的片与内存段的映射关系。
5.3.4 符号表
对比发现,hello.o颠末链接后得到hello.out,其符号数目颠末链接后也增多了。
5.3.5 重定位节
5.4 hello的虚拟地点空间
利用edb加载hello,检察本进程的虚拟地点空间各段信息,并与5.3对照分析说明。
根据程序节头表可以看出,程序从0x0000000000400040开始存储PHDR,其保存的是程序头表;从0x00000000004002e0则存储INTERP的内容,其保存的是必须要调用的解释器;而从0x0000000000401000到0x0000000000402000则对应LOAD初始段的内容。
5.5 链接的重定位过程分析
上图颠末指令objdump -d -r hello.out反汇编得到的。我们可以看到与hello.o的差别之处在于
- hello.o的反汇编文件中只有main函数的汇编代码,并没有其他函数大概其他节的代码。而hello.out的反汇编文件中不光包含了main函数的汇编代码,还链接了其他函数的汇编代码,另有其他段的信息,如_init函数,用来初始化代码的等等。
- hello.o的反汇编文件中并没有利用虚拟内存地点,而是以main函数为起点,仅利用0000000000000000来代表main函数的地点;在hello.out的反汇编文件中,则标明了每段代码的虚拟内存地点,这都是通过链接完成的。
而整个重定位过程,实际上是由链接器先将多个单独的代码节和数据节归并为单个节,就好好比.text节,颠末前后对比就可以发现,hello.out的.text节明显比hello.o的内容更多。其次,链接器便根据hello.o中.rel.text和.rel.data中的重定位条目,将代码和数据重定位到详细的虚拟地点。
5.6 hello的实行流程
- 启动edb,程序最开始位于共享库的位置,地点为0x00007f24b5fff880
- 跳转到_start函数,地点为0x00000000004010f0
- 跳转到_init,地点为0x0000000000401000
- 跳转到frame_dummy,地点为0x00000000004011d0
- 跳转到register_tm_clones,地点为0x0000000000401160
- 跳转到main函数,地点为00000000004011d6
- 跳转到printf函数,地点为0x0000000000401040
- 跳转到atoi函数,地点为0x0000000000401060
5.7 Hello的动态链接分析
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地点,所以必要添加重定位记载,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用耽误绑定的计谋。
由于edb调试难以找到dl_init,所以调试这部分临时跳过。
5.8 本章小结
本章先容了链接的概念和作用,还解说如何在Linux系统中进行链接。以及分析了hello.out的elf文件格式和虚拟空间地点。还先容了hello程序的实行过程及动态链接。
第6章 hello进程管理
6.1 进程的概念与作用
进程指一个实行中程序的实例。
作用:它提供两个假象,即似乎程序在独占地利用处理器和内存系统。
6.2 简述壳Shell-bash的作用与处理流程
shell是一个交互型的应用级程序,它代表用户运行其他程序。而Bash是shell的一个早期版本,其作用是读取用户输入的每一行指令,而且依照指令调用差别的程序。
处理流程:通过实行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行,求值步骤剖析命令行,并代表用户运行程序。
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创新一个新的运行的子进程。
界说:pid_t fork (void);
说明:子进程得到与父进程用户级虚拟地点空间相同的一份副本,而且继承父进程所有打开的文件。但是子进程与父进程的pid不一样,通常位于同一个进程组。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。
界说:int execve (const char *filename, const char *argv[], const char *envp[]);
说明:execve函数加载并运行可实行目的文件hello.out,且带参数列表argv和环境变量列表envp。而且execve会覆盖当前进程的代码、数据、栈。但是拥有和原进程一样的pid,继承已打开的文件描述符和上下文。而且只调用一次,从不返回,除非出现hello.out找不到等其他错误。
6.5 Hello的进程实行
进程提供了两种抽象:独立的逻辑控制流和私有的地点空间。在shell利用fork创建了hello进程,并用execve将其加载到内存之后,控制权便转交给了用户。而当hello进程实行到sleep函数时(即进行系统调用),则必要进行上下文切换,也就是先保存当前hello进程的上下文,然后将控制转移给内核。在内核模式下实行相应的异常处理程序,然后再将控制权转移给hello进程,此时必要规复hello的上下文。然后循环往复进行实行。所以说,在整个过程中,hello进程的控制流实际上并不是连续的,而是被分片了,但由于时间极短,所以给人的错觉就是hello进程在不停运行中。
6.6 hello的异常与信号处理
1) 乱输
由于hello进程是前台进程,于是当前输入的所有命令都不会立马实行,只是被输入到了缓冲区中。而只有前台进程终止后,才会去读取缓冲区的内容,并以换行符为标志,作为一条条命令进行实行。最开始随机输入的“niko”之所以没有实行,是因为被getchar()读走了。而后续的随机输入则保存在缓冲区中,等待hello进程终止后,被看成命令行进行实行。
2) Ctrl-C
输入Ctrl-C后,会导致内核发送一个SIGINT信号到前台进程组中的每个进程,于是hello进程便被终止了。
3) Ctrl-Z
输入Ctrl-Z后,会导致内核发送一个SIGTSTP信号到前台进程组中的每个进程,于是hello进程便被挂起了。
4) ps
将hello进程挂起后,实行ps命令,将显示当前的进程状态。可以看到hello进程虽然被挂起了,但还没被终止,仍旧保存着,而且pid为4484。
5) jobs
将hello进程挂起后,实行jobs命令,将显示当前的作业状态。上图表现此时有一个作业,而该作业处于停止的状态。
6) pstree
利用pstree命令可以看到整个进程的树结构。此中就可以找到gnome-terminal中的bash进程中的子进程:hello.out进程和pstree进程。
7) fg
利用fg指令,将对应序号的作业放到前台运行。如上图,可以看到将序号1的作业hello放到前台运行,此时随机输入命令仍旧无法响应。
8) bg
利用bg指令,将对应序号的作业放到后台实行。在此过程中,hello照常输出,同时输入的命令也可以得到响应,并不影响后台作业的运行。对于hello进程来说,循环8次后,还必要getchar才能结束进程。而又因为hello进程是后台作业,所以此时输入的任何字符串并不会被getchar读取,所以hello进程仍旧存在。只有当把该后台作业切换到前台时,再输入字符串,才能结束该hello进程。
9) kill
在发送Ctrl-Z将hello进程挂起后,便可以利用kill -18 4686来发送SIGCONT给hello进程,让其进行运行。但是由于此时hello进程时后台作业,所以想要终止hello进程,还必要利用kill -9 4686(大概时切换到前台,然后随机输入字符串进行终止)。
6.7本章小结
本章先容了进程的概念和作用,以及shell时如何调用fork和execve来生成和加载hello进程的。同时,还分析了hello进程的实行过程,以及一些异常和信号时如何处理的。
第7章 hello的存储管理
7.1 hello的存储器地点空间
- 逻辑地点:指由程序产生的段内偏移地点。
- 线性地点:指虚拟地点到物理地点变换的中央层,是处理器可寻址的内存空间(称为线性地点空间)中的地点。程序代码会产生逻辑地点,大概说段中的偏移地点,加上相应段基址就成了一个线性地点。
- 虚拟地点:由程序产生的由段选择符和段内偏移地点组成的地点。也就是hello程序中链接后得到的数据代码地点。
- 物理地点:用于内存芯片级内存单位寻址。
7.2 Intel逻辑地点到线性地点的变换-段式管理
对于一个以“段地点+偏移地点”形式给出的逻辑地点,CPU将会通过此中的16位段选择子定位到GDT/LDT中的段描述符,通过这个段描述符得到段的基址,与段内偏移地点相加得到的64位整数就是线性地点。此中,段的划分(即GDT和LDT都)是由操作系统内核控制的。
7.3 Hello的线性地点到物理地点的变换-页式管理
虚拟空间被划分成若干页,采用分页机制进行管理。CPU会通过线性地点的高位和内存中的页表去查询其对应的页表条目。然后将线性地点的低位(即偏移量)与所对应的页表条目中所记载的物理页号进行拼接,便能得到物理地点。
7.4 TLB与四级页表支持下的VA到PA的变换
CPU采用多级页表来压缩页表的巨细,采用TLB来增快访问页表条目的速率。所以当CPU产生一个虚拟地点时,首先会由MMU到TLB中进行查询,若TLB命中,则能直接得到页表条目,于是便能由MMU直接将虚拟地点转换为物理地点;若TLB不命中,则必要先通过VPN1到主存中去寻找一级页表,然后再通过一级页表去寻找二级页表(若二级页表不在主存中,则发生缺页故障,将从磁盘中将其调入),然后由VPN2在二级页表中寻找三级页表的地点,以此类推,直到找到四级页表所对应条目的PPN,最后便完成VA到PA的变换。
7.5 三级Cache支持下的物理内存访问
CPU利用程序局部性原理,采用Cache机制来增快访问主存的速率。而CPU得到物理地点后,会先访问一级Cache,若命中,则直接将相应的数据块调给CPU既可;若不命中,则在访问二级Cache,同理,若仍不命中,则再访问三级Cache,直到全不命中的时间,再去访问主存,并将相应的数据块调入到L1、L2、L3 Cache中。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一 的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct, vm_area_struct和页表的原样副本。它将两个进程中的每个页面都标志为只读,并将两个进程中的每个地域结构都标志为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个厥后进行写操作时,写时复制机制就会创建新页面。因此,也就为每个进程保持了私有地点空间的抽象概念。
7.7 hello进程execve时的内存映射
Execve函数会先删除已存在的用户地域,然后新程序的代码、数据、bss和栈地域创建新的地域结构。此中,代码和初始化数据映射到.text和.data区(目的文件提供),而.bss和栈映射到匿名文件。另外。还必要映射共享地域。最后,必要设置当前进程上下文的PC,使之指向代码地域的入口点。
7.8 缺页故障与缺页停止处理
当CPU实行某条指令的内存访问时,如果页表中的PTE表明这个地点对应的页不在物理内存中,那么就会引发缺页故障。此时必要进行上下文切换,将控制权传递给缺页处理程序,然后该程序将相应的页从磁盘调入到内存中,处理完毕后,便会返回,进行上下文切换,重新实行当前的指令。
7.9动态存储分配管理
系统通过动态内存分配器来管理内存。动态内存分配器维护着一个进程的虚拟内存地域,称为堆。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
通过隐式空闲链表,分配器可以通过对于链表的操作以完成在堆上放置已分配的块、分割空闲块、获取额外内存、归并空闲块等操作。于是应用程序就可以动态地在堆上分配额外内存空间了。
7.10本章小结
本章重要先容了hello的存储地点空间,包括段式管理和页式管理,以及VA到PA的转换过程和Cache的访问流程。还讲述了fork和execve的内存映射,以及CPU是如何处理缺页故障的。最后还简单先容里动态存储分配管理机制。
结论
hello.c被程序员利用编辑器编写出来,这就是hello的起点。接着颠末预处理处理,插入了所有效#include命令指定的文件后,酿成了hello.i文件。之后被编译器翻译为汇编语言,酿成了hello.s文件。然后,继续被汇编器转化为呆板语言,酿成hello.o文件。但是此时还不能被实行。因为还必要进行符号剖析和重定位(也就是确定代码和数据的地点)。所以还必要颠末链接器进行链接,最后才能得到可实行目的文件hello.out。
然后,在bash中,被由操作系统调用fork和execve为hello.out生成一个进程,并把它加载到内存。此时,控制权已经转交给了hello进程,hello进程可以尽情地进行printf,直到hello进程接收到进程终止的信号。然后,被父进程bash接纳掉。现在,hello便结束了自己的一生。
通过学习计算机系统这门课,我对计算机有了一个全新的认识。第一次相识了一个源文件是如何被编译成可实行文件的,没有想到这其间另有如此之多的工序。也是第一次相识一个程序是如何被加载运行的。尤其是学习了进程、作业、上下文等等这些概念,使我对CPU的使命管理有了更加底层的相识。同时,也是第一次学习到计算机的内存管理,包括Cache机制和虚拟内存技术,这一切感觉都是那么的新颖有趣。
总之,非常喜欢这门课,它带给计算机初学者更底层更详细的看法,也会后续课程的进阶打下了坚固基础。
附件
文件名说明hello.i由预处理器生成的文件hello.s由编译器生成的文件hello.o由汇编器生成的文件hello.out由链接器生成的文件o_ans.txtHell.o反汇编生成的文件out_ans.txtHello.out反汇编生成的文件 参考文献
[1] http://csapp.cs.cmu.edu/
[2] https://zhuanlan.zhihu.com/p/476697014
[3] https://blog.csdn.net/yfldyxl/article/details/81566279
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |