计算机科学与技术学院
2022年5月
摘 要
本文通过对hello程序在linux系统中的一生的研究,探讨预处理编译,汇编,链接,进程,储存的过程,深入理解hello.c程序,结合《深入理解计算机系统》内容,对本学期所学的知识进行梳理总结,加深对计算机系统的理解
关键词:计算机系统;预处理;编译;汇编;链接;进程;储存
目 录
第1章 概述................................................... - 4 -
1.1 Hello简介............................................ - 4 -
1.2 环境与工具........................................... - 4 -
1.3 中间结果............................................... - 4 -
1.4 本章小结............................................... - 4 -
第2章 预处理............................................... - 5 -
2.1 预处理的概念与作用........................... - 5 -
2.2在Ubuntu下预处理的命令................ - 5 -
2.3 Hello的预处理结果解析.................... - 5 -
2.4 本章小结............................................... - 5 -
第3章 编译................................................... - 6 -
3.1 编译的概念与作用............................... - 6 -
3.2 在Ubuntu下编译的命令.................... - 6 -
3.3 Hello的编译结果解析........................ - 6 -
3.4 本章小结............................................... - 6 -
第4章 汇编................................................... - 7 -
4.1 汇编的概念与作用............................... - 7 -
4.2 在Ubuntu下汇编的命令.................... - 7 -
4.3 可重定位目标elf格式........................ - 7 -
4.4 Hello.o的结果解析............................. - 7 -
4.5 本章小结............................................... - 7 -
第5章 链接................................................... - 8 -
5.1 链接的概念与作用............................... - 8 -
5.2 在Ubuntu下链接的命令.................... - 8 -
5.3 可执行目标文件hello的格式........... - 8 -
5.4 hello的虚拟地址空间......................... - 8 -
5.5 链接的重定位过程分析....................... - 8 -
5.6 hello的执行流程................................. - 8 -
5.7 Hello的动态链接分析........................ - 8 -
5.8 本章小结............................................... - 9 -
第6章 hello进程管理.......................... - 10 -
6.1 进程的概念与作用............................. - 10 -
6.2 简述壳Shell-bash的作用与处理流程.. - 10 -
6.3 Hello的fork进程创建过程............ - 10 -
6.4 Hello的execve过程........................ - 10 -
6.5 Hello的进程执行.............................. - 10 -
6.6 hello的异常与信号处理................... - 10 -
6.7本章小结.............................................. - 10 -
第7章 hello的存储管理...................... - 11 -
7.1 hello的存储器地址空间................... - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................................ - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理........................................................ - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换................................................................ - 11 -
7.6 hello进程fork时的内存映射......... - 11 -
7.7 hello进程execve时的内存映射..... - 11 -
7.8 缺页故障与缺页中断处理................. - 11 -
7.9动态存储分配管理.............................. - 11 -
7.10本章小结............................................ - 12 -
第8章 hello的IO管理....................... - 13 -
8.1 Linux的IO设备管理方法................. - 13 -
8.2 简述Unix IO接口及其函数.............. - 13 -
8.3 printf的实现分析.............................. - 13 -
8.4 getchar的实现分析.......................... - 13 -
8.5本章小结.............................................. - 13 -
结论............................................................... - 14 -
附件............................................................... - 15 -
参考文献....................................................... - 16 -
第1章 概述
1.1 Hello简介
P2P即Program to Process,是指将hello.c从可执行文件变为进程的过程,先后经过预处理、编译、汇编、链接得到可执行文件,最后在shell中创建进程。
020即From 0 to 0,详细理解为:初始时内存中并没有hello.c的相关内容,在shell中调用execve函数,hello.c的内容将载入内存中,并执行相关代码。在程序运行完毕后,进程被回收。
1.2 环境与工具
1.2.1 硬件环境
AMD Ryzen 7 6800H with Radeon Graphics 3.20 GHz
AMD Radeon Graphics 680m
16.0GB (13.7GB可用)
512GB固态硬盘
1.2.2 软件环境
Windows 11 家庭中文版 22H2
Visual Studio Community 2022
VMware Workstation Pro 17
Ubuntu 22.04.2 LTS
1.2.3 开发工具
Visual Studio Community 2022
Code::Blocks IDE
EDB
1.3 中间结果
hello.i | 预处理后得到的文本文件 |
hello.s | 编译后得到的汇编语言文件 |
hello.o | 汇编后得到的可重定位目标文件 |
hello.elf | 用readelf读取hello.o得到的ELF格式信息 |
hello | hello.c的可执行文件 |
1.4 本章小结
介绍了P2P和020的过程,并给出了实验环境,以及期中生成的文件
第2章 预处理
2.1 预处理的概念与作用
预处理是在编译之前的操作,指在程序开始运行时,预处理器根据以#起始的命令,修改原始的C语言,并生成一个XXX.i的文件的过程。
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
图1 预处理hello.c
2.3 Hello的预处理结果解析
发现很短的hello.c变成了3091行的文本文件,因为预处理过程中将stdio.h 、unistd.h、 stdlib.h展开,而原hello.c的main函数在末尾
图2 预处理结果(部分)
图3 main函数在hello.i的部分
2.4 本章小结
本章对预处理进行的分析,阐述了预处理过程的概念和作用,并在linux下执行了hello.c的预处理过程,生成了hello.i文件,并对hello.i文件进行了初步的分析。
第3章 编译
3.1 编译的概念与作用
编译为通过分析(词法分析、语法分析),将合法的指令翻译成汇编代码的过程。具体概念为cll将合法的.i文件翻译为.s文件
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
图4 编译hello.i
3.3 Hello的编译结果解析
3.3.1 数据:
常量:
函数中的if语句的4保存在.text中, for中的0、9、1、2、3也保存在.text中。
printf()字符串将会保存在.rodata中
变量:有局部变量i被保存在栈上
3.3.2 赋值:
赋值操作有for语句中的=,为给i赋值0
3.3.3 算术操作:
算数操作有for循环中的i++;给i+1,由add指令来实现,
3.3.4 关系操作:
关系操作有两个,分别是if语句中的!= 和for语句中的<
3.3.5 数组/指针/结构操作:
数组/指针/结构操作有main函数中接收的指针数组char *argv[]
3.3.6 控制转移:
控制转移有三个,一个是if语句,一个是无条件跳转,一个是for语句中的判断成分
3.3.7 函数操作:
函数操作通过call指令调用过程
3.4 本章小结
本章阐述了编译的概念、作用和过程。介绍了汇编代码对应的各种数据、操作。
第4章 汇编
4.1 汇编的概念与作用
汇编是将汇编语言转化为机器语言指令,并把这些指令打包成一个可重定位目标文件的格式,生成目标文件.o文件
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
!
图5 生成hello.o
4.3 可重定位目标elf格式
典型的ELF格式的可重定位目标文件的结构如下:
图6 elf结构
生成hello.o文件elf格式的命令:readelf -a hello.o
图7 显示elf格式
通过如上图可知:hello.o中共有13个节,8个重定位条目,7个全局符号
4.4 Hello.o的结果解析
输入objdump -d -r hello.o得到了hello.o中执行代码的反汇编结果
图8 反汇编结果
经过对比发现hello.o反汇编代码和hello.s文件基本一样,只有部分不同。
- 分支转移不同,.s文件是通过指令jmp,je等直接跳转到对应的代码。而反汇编代码中,转移时通过地址进行跳转的。
- 函数调用不同, .s文件中是通过call+函数名称进行调用函数。而在反汇编代码中,call后跟的是下一条指令。因为这些函数都是共享库函数,地址是不确定的,所以call指令后面还会有为链接专门空出的空间,等待链接器的下一步的重定位、链接。
- 全局变量不同, .s文件中,使用段地址+%rip访问rodata完成的,而在反汇编代码中,则是:0+%rip进行访问,因为.rodata节中的数据是在运行时确定的,也需要重定位,将操作数设为0并给.rela.text添加重定位条目。
4.5 本章小结
本章阐述了汇编的概念和作用,并通过生成的elf文件对eld格式结构进行了研究,比对了.asm和.s的相同和不同之处。
第5章 链接
5.1 链接的概念与作用
链接是指通过链接器将不同文件的数据和代码综合到一起,并生成一个可以在程序中加载和运行的单一的可执行目标文件的过程
5.2 在Ubuntu下链接的命令
命令:ld -o hello.o -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/9/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello
图9 生成可执行文件
5.3 可执行目标文件hello的格式
命令:readelf -a hello
ELF头:hello的ELF头和hello.o的ELF头基本相同,不同之处:hello为可执行文件,节头有27个,type为EXEC
图10 elf头
.节头:包括了对所有节的声明。
图11 节头
程序头表:
图12 程序头表
动态节:
图13 动态节
可重定位条目:
图14 可重定位条目
符号表
图15 符号表
版本信息
图16 版本信息
5.4 hello的虚拟地址空间
从程序头可以看出客家在的地址为0x400000,
图17 程序头
使用edb打开hello,从Data Dump找到0x400000:
图18 Data Dump
也可以根据5.3中的信息,在edb中找到各段的信息
如.text节的信息:
图19 .text对应的Data Dump表
5.5 链接的重定位过程分析
objdump -d -r hello
图20 反汇编结果
不同:可执行文件的反汇编结果的内容中多了很多函数代码,这些函数是链接器把共享库中hello.c用到的函数也加入到文件中;另一个不同是地址不同,因为重定位后把相对地址替换为了虚拟地址(重定位后)或绝对地址(call指令跳转)。
重定位分析:
- 重定位节和符号定位:链接器将所有相同类型的节合并为同一类型的聚合节,然后链接器将内存地址给新的聚合节、输入模块定义的每个节和符号。
- 重定位节中的符号引用:链接器修改代码节和数据节中对每个符号的引用,让他们指向正确的运行时地址。
5.6 hello的执行流程
记录如下:
401000 <_init>
401020 <.plt>
401030 <puts@plt>
401040 <printf@plt>
401050 <getchar@plt>
401060 <atoi@plt>
401070 <exit@plt>
401080 <sleep@plt>
401090 <_start>
4010c0 <_dl_relocate_static_pie>
4010c1 <main>
401150 <__libc_csu_init>
4011b0 <__libc_csu_fini>
4011b4 <_fini>
图21 edb运行程序
5.7 Hello的动态链接分析
动态链接为把程序拆分成各个相对独立的部分,在程序运行的时候将其链接起来。在调用共享库函数的时候编译器为其生成一条重定位记录,在程序加载的时候链接器再解析它。
由图可知got起始表位置在0x404000
调用之前:
图22 调用之前
调用之后:
图23 调用之后
5.8 本章小结
本章阐述了链接的概念和作用,通过edb查看,和hello和hello.o的反汇编代码的区别,探讨了链接中重定位的过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程是指一个执行中的程序的实例,或者可以定义为一个具有一定独立功能的程序关于某个数据集合的一次运行活动。进程是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
6.2 简述壳Shell-bash的作用与处理流程
- 终端输入./hello
- Shell解释器构造argv和envp
- 调用fork()创建子进程
- 调用execve()再当前进程的上下文加载并运行hello程序
- 调用hello中的main函数。
6.3 Hello的fork进程创建过程
获取执行命令之后,父进程判断是否是内部指令,如果是就立即执行,如若不是就通过fork函数创建子进程,子进程地址空间与父进程完全相同。Fork函数只被调用一次,但是会返回两次,父进程返回子进程的UID,子进程中,返回0。子进程运行结束后,如果父进程还存在,就对子进程回收,不然就由init进程回收子进程。
6.4 Hello的execve过程
Execve函数在当前进程的上下文中加载并运行一个新程序。execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。与fork一次调用返回两次不同,execve调用一次,不返回。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
进程上下文:一般在进程切换中提到,进程控制块PCB,保存着进程的诸多详细信息,当进程要切换时当前进程的寄存器内容以及内存页表的详细信息等等内容,也就是关于描述进程的信息。
进程时间片:即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。
进程调度:当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用上下文切换机制将控制转移到新的进程。
用户态与核心态:进程hello初始运行在用户模式中,知道它通过执行系统调用exit,sleep和getchar时便陷入内核。内核中的陷阱处理程序完成对系统函数的调用。7之后,内核执行上下文切换,将控制返回给hello紧随系统调用之后的那条语句。
6.6 hello的异常与信号处理
正常运行结果:
图24 正常运行结果
不停乱按:不影响进程执行
图25 乱按结果
按回车:
图26 按回车结果
按Ctrl+C:收到SIGTSTP,进程中断。
图27 按Ctrl+C结果
按Ctrl+Z:进程暂停
图28 按Ctrl+Z结果
之后运行ps:获取所有进程的状态
图29 运行ps结果
运行jobs:打印所有作业
图30 运行jobs结果
运行Fg:使暂停的进程继续作为前台作业运行,
图31 运行fg结果
运行Kill:终止处于暂停的hello作业。
图32 运行kill结果
6.7本章小结
本章阐述了进程的概念和作用和shell的作用和过程,同时了解到异常与信号机制。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址
线性地址:跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址
虚拟地址:这是对整个内存的抽像描述。它是相对于物理内存来讲的,可以直接理解成“不直实的”,“假的”内存,
物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址为段选择符+偏移量,每个段选择符为16为大小,段描述符为8字节,CPU会通过段选择定位到GDT/LDT中的段描述符,通过这个段描述符得到的段的基址,和段内偏移地址加起来就是线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存被分为若干页,CPU取一个线性地址的高若干位,通过他们在页表里查询对应的页表条目,得到对应的物理页起始地址,然后和线性地址的低位相加就是物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
以Intel i7为例,i7为四级页表。
CPU产生VA(虚拟地址),传给MMU(内存管理单元),MMU向TLB寻找,如果命中就返回PA(物理地址),如果没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,同理,最后在第四级页表找到PPN,PPN和VPO组合成PA,
7.5 三级Cache支持下的物理内存访问
CPU为了快速访问内存,使用三级Cache缓存,将之前访问过的内存块存在缓存中,当CPU对一个物理地址访问时,先去L1查看是否有对应的内存块,如果有就直接访问L1,如果没有就依次L2,L3访问,如果都没有就去内存访问,并将块加载到L3L2L1缓存上。
7.6 hello进程fork时的内存映射
Fork函数被调用时,将会创建子进程,并为子进程提供各种数据结构,分配这个子进程一个PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有写时复制。
当 fork 在新进程中返回时,新进程现在的虚拟内存刚好和调用 fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数加载并运行hello需要以下几个步骤:
- 在bash中的进程中执行execve调用
- execve函数在当前进程中加载并运行包含在可执行文件hello中的程序,用hello替代了当前bash中的程序。
3. 删除已存在的用户区域
4. 映射私有区域
5. 映射共享区域
6. 设置程序计数器
7.8 缺页故障与缺页中断处理
缺页故障指的是当软件试图访问已映射在虚拟地址空间中,但是目前并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元所发出的中断。
缺页中断处理:
(1) 保留进程上下文
(2)判断内存是否有空闲可用帧,若有,则获取一个帧号No,转(4) 启动I/O过程。若无,继续(3)
(3)腾出一个空闲帧,即:
(3)-1调用置换算法,选择一个淘汰页PTj。
(3)-2 PTj(S)=0 ; //驻留位置0
(3)-3 No= PTj (F); //取该页帧号
(3)-4 若该页曾修改过,则
(3)-4-1 请求外存交换区上一个空闲块B ;
(3)-4-2 PTj(D)=B ; //记录外存地址
(3)-4-3启动I/O管理程序,将该页写到外存上。
(4)按页表中提供的缺页外存位置,启动I/O,将缺页装入空闲帧No中。
(5)修改页表中该页的驻留位和内存地址。PTi(S)=1 ; PTi(F) =No。
(6)结束。
7.9动态存储分配管理
有些操作对象只有在程序运行时才能确定,这样编译器在编译时就无法为他们预定存储空间,只能在程序运行时,系统根据执行的需要,部分地动态装入。同时,在装入主存的程序不执行时,系统可以收回该程序所占据的主存空间。这种方法称为动态内存分配。所有动态存储分配都在堆区中进行。
再者,用户程序装入主存后的位置,在运行期间可根据系统需要而发生改变。此外,用户程序在运行期间也可动态地申请存储空间以满足程序需求。由此可见,动态存储分配方式在存储空间的分配和释放上,表现得十分灵活,现代的操作系统常采用这种存储方式。
7.10本章小结
本章阐述了存储的机制,描述了四种地址的定义,给出了fork函数和execve函数的内存映射,诠释了动态存储分配管理
结论
通过大作业对这7部分的分析,我们可以用以下这一系列步骤来阐述hello所经历的一切:
- 预处理:对hello.c文件进行预处理(cpp),得到hello.i文件
- 编译:通过分析(词法分析、语法分析)将合法指令翻译成汇编代码,得到hello.s文件
- 汇编:将hello.s文件翻译成一个可重定位目标文件hello.o
- 链接:把hello.o文件和可重定位目标文件和动态库链接起来,生成可执行文件hello
- 运行:在shell中运行 ./hello
- 进程:使用fork函数创建子进程
- 加载:使用execve函数为新进程把代码和数据载入虚拟内存空间,开始执行程序
- 执行指令:CPU分配时间片,hello享有CPU资源,顺序执行自己的控制逻辑流
- 访问内存:通过MMU将逻辑地址映射成物理地址,
- 动态内存:printf会调用malloc函数向动态内存分配器申请堆中的内存
- 信号处理:键入ctrl+c或ctrl+z对进程进行操作,+c是停止,+z是挂起
- 结束:父进程等待并回收子进程,内核删除为进程创建的数据结构
感悟:通过这次实验,我深刻的感受到计算机的功能强大和结构操作精细,运行一个小小的hello.c也需要背后大量的机制操作来支撑。驻足回望,从接触计算机的第一个hello world到如今的hello.c,变化的是年龄,见识,知识,不变的是对计算机科学的一片热忱!
附件
hello.i | 预处理后得到的文本文件 |
hello.s | 编译后得到的汇编语言文件 |
hello.o | 汇编后得到的可重定位目标文件 |
hello.elf | 用readelf读取hello.o得到的ELF格式信息 |
hello | hello.c的可执行文件 |
参考文献
[1] 《深入理解计算机系统》 Randal E.Bryant David R.O’Hallaron 机械工业出版社
[2] https://blog.csdn.net/qq_43101637/article/details/106646554
[3] https://blog.csdn.net/TYUTyansheng/article/details/108148566