计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021111050
班 级 2103101
学 生 吕泽楷
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年11月
摘 要
HelloWorld是每个程序员接触的第一个程序,本文观察hello.c 文件在Linux系统中的生命周期,从预处理编译开始,观察其如何一步步生成hello可执行文件,从计算机底层解释了存储、分配、I/O过程、子程序的创建和回收等过程,使我们对计算机和程序有直观深刻的认识。
关键词:Liunx;P2P;计算机系统;hello
目 录
第1章 概述
1.1 Hello简介
1.2 环境与工具
1.3 中间结果
1.4 本章小结
第2章 预处理
2.1 预处理的概念与作用
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
2.4 本章小结
第3章 编译
3.1 编译的概念与作用
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.4 本章小结
第4章 汇编
4.1 汇编的概念与作用
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
4.4 Hello.o的结果解析
4.5 本章小结
第5章 链接
5.1 链接的概念与作用
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
5.4 hello的虚拟地址空间
5.5 链接的重定位过程分析
5.6 hello的执行流程
5.7 Hello的动态链接分析
5.8 本章小结
第6章 hello进程管理
6.1 进程的概念与作用
6.2 简述壳Shell-bash的作用与处理流程
6.3 Hello的fork进程创建过程
6.4 Hello的execve过程
6.5 Hello的进程执行
6.6 hello的异常与信号处理
6.7本章小结
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.2 Intel逻辑地址到线性地址的变换-段式管理
7.3 Hello的线性地址到物理地址的变换-页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
7.5 三级Cache支持下的物理内存访问
7.6 hello进程fork时的内存映射
7.7 hello进程execve时的内存映射
7.8 缺页故障与缺页中断处理
7.9动态存储分配管理
7.10本章小结
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
8.2 简述Unix IO接口及其函数
8.3 printf的实现分析
8.4 getchar的实现分析
8.5本章小结
结论
附件
参考文献
1.1.1 P2P:
P2P即From Program to Process。
Program指hello.c源文件,是经过我们键盘输入后读入内存的文件;
Process指进程,hello.c文件经过预处理(cpp)、编译(ccl)、汇编(as)、链接(ld)最终生成可执行文件hello。当我们在shell中输入./hello 学号 姓名 秒数,shell为其创建子进程,接着把用execve把hello的内容加载到子进程中运行,实现了由Process向Process的转变,这就是P2P。
1.1.2 020:
020即From Zero-0 to Zero-0。程序运行前,execve将hello程序加载到相应的上下文中,从main函数开始执行代码;在程序结束后shell回收进程,释放hello的内存删除上下文,清除痕迹。整个hello程序就是从无到有再到无的过程,即From Zero-0 to Zero-0。
硬件:X64CPU
软件:Windows10 64位 Vmware Ubuntu 20.03
开发工具:GCC GDB objdump VisualStudio2019 Codeblocks
文件名 |
文件作用 |
hello.c |
源文件 |
hello.i |
hello.c经过预处理(cpp)后的文本文件 |
hello.s |
hello.i经过编译(ccl)得到的汇编文件 |
hello.o |
hello.s经过汇编(as)得到的可重定位目标文件 |
hello |
hello.o与其他目标文件链接后得到的可执行目标文件 |
elf.txt |
hello.o的elf文件 |
hello_elf.txt |
hello的elf文件 |
本章介绍了hello的P2P过程和020过程,初步介绍了hello的程序人生,介绍了环境和工具,说明了处理过程的各项中间结果为本论文奠定基础。
2.1.1概念:预处理是源文件编译前需要做的预备工作,预处理器(cpp)会修改原始的C程序,会将以#开始的代码解释为预处理指令,比如宏定义(#define)、文件包含(include)、条件编译(#if)等,将这些内容直接插入到程序文本,删除注释和多余空白字符。
2.1.2作用:源文件经过预处理可以得到便于编译器工作的.i文件
在程序所在终端输入gcc -E hello.c -o hello.i得到预处理文件后如下
图1预处理后文件
图2源文件
观察源文件可以看到由三个#include可以在预处理时进行操作
打开hello.i文件:
图3 hello.i文件
可以看到源文件已经扩展为3091行,其中注释已经被删除
图4 文件包含预处理
搜索三个.h文件可以看到其调用路径已经被预处理器写入.i文件,说明
#include
3.1.1概念:编译是指编译器将源程序转换为计算机可以识别的机器语言——汇编语言,编译可以分为分析和整合两部分,分析过程将源程序分成多个结构,校验其格式,收集源程序信息,并将其放在符号表中;整合过程根据分析过程传递的信息构造目标程序。最后生成hello.s文件
3.1.2 作用:编译共六个步骤:
输入编译指令获得编译文件:
图5生成编译文件
3.3.1数据保存:
在函数中,常量包括printf函数的字符串,if(argc!=4)中的常数4和for(i=0;i<9;i++)中常数9,其中字符串保存在.rodata节中,数字常量在.text节中,以立即数形式存入栈中,如图:
图6 字符串常量
图7 常量4存入栈
图8 常量9操作,存入8作为比对小于9的数
3.3.2变量存储:
hello.c文件不存在全局变量,存在局部变量i,指针数组argv[]和局部变量argc
钧保存在栈中:
图9 局部变量argv、argc保存
在图8中可以看到栈指针%rsp减32创造栈空间,第一个参数argc保存在%rbp-20地址处,第二个参数argv保存在地址%rbp-32处
图10 局部变量i保存:
局部变量i保存在%rbp-4地址处,并赋初值0;
3.3.3算数操作
图11 i++算数操作
在for循环中i每次循环加1,addl指令将%rbp-4地址对应内存处的值加一,等价于i++;
3.3.4 条件判断和exit函数调用
图12 if条件语句
If判断argc值,若argc不等于4,打印字符串并结束进程
图13 对应汇编语言
cmpl指令将%rbp-20地址处保存的argc与立即数4比较,若相等,跳转到L2(如图14);若不相等将.LC0保存的字符串打印,并调用exit函数。
3.3.5 控制转移和getchar函数调用
图14 在if判断argc为4后进行for循环
图15 for循环判断
在L2中将i设为1后跳转到L3。
在L3中jle指令判断i与8的关系,若i小于等于8,跳转到L4(如图15),否则调用getchar函数。
最后用ret指令返回0;
3.3.6 函数调用和数组操作
在for循环内部调用了printf函数,sleep函数,atoi函数
图16 for循环内部
调用printf函数时%edi保存第一个参数:字符串.LC1;%rsi保存第二个参数argv[1],%rdx保存第三个参数argv[2]他们由栈指针%rax从栈中赋值。
调用sleep函数时%rax+24从栈中获取参数argv[3]并赋给%rdi,调用函数atoi将字符串转换为int类型,最后调用sleep函数
本章聚焦hello.s文件,对照源文件一步一步解析汇编指令,分析数据储存,变量保存,函数调用,控制转移,数组操作等如何用汇编语言一步步实现,并分析了汇编操作中寄存器和栈的变化
4.1.1概念:汇编器将.s文件翻译成机器语言指令,并将这些指令打包成可重定位目标文件的格式,可重定位目标文件包括二进制的代码和数据。
4.1.2作用:将汇编语句翻译成二进制机器指令,便于后续链接
图17 汇编命令
图18 查看elf文件
用readelf命令查看hello.o的elf文件:
图19 ELF头
ELF头由以16字节序列Magic开始,描述了生成该文件的系统的字的大小和字节顺序,剩下部分包含 ELF 头的大小、目标文件的类型、机器类型、 字节头部表(section header table)的文件偏移,及节头部表中条目的大小和数量
图20 节头表
解析来的节头表描述了不同节的位置和大小:
1、.text:已编译程序的机器代码。
2、.rodata:只读数据。
3、.data:已初始化的全局变量和静态C变量。
4、.bss:未初始化的全局变量和静态C变量,仅是占位符,不占据任何实际磁盘空间。
5、.symtab:符号表,存放程序中定义和引用的函数和全局变量信息,不包括局部变量的条目。
6、.rel.text:.text节的重定位信息,用于重新修改代码段的指令中的地址信息。
7、.rel.data:.data节的重定位信息,用于对被模块使用或定义的全局变量重定位的信息。
8、.debug:调试符号表,只有以-g方式调用编译器驱动程序时,才会得到这张表。
9、.line:原始C源程序中的行号和.text节中机器指令之间的映射。
10、.strtab节:字符串表,包括.symtab和.debug节中的符号表。
图21 重定位节
重定位节是一个text节中位置的列表,包含text节中需要进行重定位的信息,用于在链接是修改指令中的地址
使用objdump -d -r hello.o指令获得hello.o的反汇编如下:
图22反汇编文件
对比hello.s中:
图23 hello.s与反汇编文件中数字的对比
图24 hello.s与反汇编文件中分支转移的对比
在汇编文件中跳转需要L2,在反汇编文件中用十六进制相对地址来让jmp指令找到需要跳转的地方
图25 hello.s与反汇编文件中函数调用的对比
hallo.s中,call指令使用的是函数名称,在反汇编文件中使用相对偏移地址
本章中首先介绍了汇编的概念和作用,接着对hello.s文件进行汇编,生成可重定位目标文件hello.o,之后使用readelf工具,查看了hello.o的ELF头、节头表,可重定位信息和符号表等,接着对hello.o文件和反汇编文件进行了对比,分析了机器语言与汇编语言的一一对应关系
5.1.1 概念:
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
5.1.2 作用:
使用ld链接指令链接形成hello可执行文件:
图26 链接指令生成可执行文件
用readelf查看hello可执行文件hello的ELF文件:
图27 hello程序的ELF文件的ELF头
与hello.o的ELF文件相比,入口地址由0x0变为了0x4010f0,程序的节和起始地址和大小变化,多了13个节
图28 hello程序的节
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
图29 程序节
elf可执行文件易加载到内存,可执行文件连续的片被映射到连续的内存段,程序头部表描述了这一映射关系。程序头部表包括各程序头的名称、类型、偏移量、内存地址、对其要求、目标文件与内存中的段大小及运行时访问权限等信息。
图30 重定位节
图31 edb查看hello文件的虚拟内存
对比ELF节内容
图32 .init的位置
在ELF节头中.init显示在40100地址位置
在虚拟内存中对应:
图33.init在虚拟内存中的内容
依次可找到其他节头在虚拟内存中的位置
使用objdump -d -r hello 查看hello程序的反汇编如下:
图34 hello程序反汇编
在hello的反汇编文件中多了很多节和函数,如.init节 和puts函数,在链接过程中,加入了库函数,并进行了重定位
在hello.o的反汇编中,main函数是从0地址开始的,在hello中保存的虚拟内存的地址为401125
在hello.o的反汇编中存在重定位条目,在hello的反汇编中,链接的函数如puts、printf、getchar等都有了分配到虚拟内存的地址,函数调用确定了这些函数重定位后的地址,可以直接执行。
图35 链接的函数
在重定位过程中:
图36 hello.o中的puts函数
图37 hello中的puts调用
分析hello.o中的puts可以发现与main函数偏移地址为1f,格式为R_X86_64_PLT32 调用后位置偏移量为-0x4
所以(unsigned)(0x401090 + (-0x4) - (0x401125 + 0x1f))=0xff ff ff 48,转换为小端法即为48 ff ff ff
图38 在edb中执行hello
地址 |
子程序名 |
0x4010f0 |
_start |
0x401000 |
_init |
0x401125 |
main |
0x401090 |
puts@plt |
0x4010d0 |
exit@plt |
0x4011b4 |
_fini |
0x4010e0 |
sleep@plt |
0x4010c0 |
atoi@plt |
0x4010a0 |
printf@plt |
0x4010b0 |
getchar@plt |
0x401120 |
_dl_relocate_static_pie |
0x401020 |
.plt |
动态的链接器在正常工作时链接器采取了延迟绑定的链接器策略,将过程地址的绑定推迟到第一次调用该过程时。
GOT表中存放着函数的目标地址,PLT表则使用GOT中的地址来跳转到目标函数.首先找到GOT地址
图39 ELF文件中.got地址
图40 dl_init前
图41 dl_init后
本章介绍了链接的概念和作用,详细说明了链接的过程,可执行文件hello的ELF头和可执行文件hello的反汇编过程,对链接的重定位过程进行了分析,对hello的动态链接过程进行了分析。
6.1.1概念:进程是一个执行中程序的实例,系统中的每个程序都运行在某个进程的上下文中。它是操作系统进行资源分配的基本单位,也就是说每启动一个进程,操作系统都会给其分配一定的运行资源(内存资源)保证进程的运行。
6.1.2作用:进程可以使一个系统并发执行多个任务;
进程提供给应用程序关键的抽象:一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器;一个四月的地址空间,它提供一个假象,好像我们的程序独占的使用内存系统
6.2.1作用:
shell是一个交互型应用级程序,代表用户运行其他程序,用户通过其提供的界面访问操作系统内核的服务,是用户使用 Linux 的桥梁。shell接收用户命令,然后调用相应的应用程序。
6.2.2处理流程:
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
(4)如果不是内部命令,调用fork( )创建新进程/子进程
(5)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(6)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid()
(7)如果用户要求后台运行(如果命令末尾有&号),则shell返回;
父进程调用fork函数创建一个新的运行的子进程,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据短、堆、共享库以及用户栈,子进程获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork()函数时,子进程可以读取父进程中打开的任何文件。子进程有不同于父进程的PID。
6.5.1上下文信息:上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
6.5.2 逻辑控制流:进程运行过程中PC值的序列叫做逻辑控制流。
6.5.3 进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
6.5.4调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。
6.5.5上下文切换:当内核决定抢占当前进程时,需要进行上下文切换,其过程通常包括以下三个步骤: ①保存当前进程的上下文;②恢复某个先前被抢占的进程被保存的上下文;③将控制传递给这个新恢复的进程。
6.5.6 用户模式和内核模式
处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任 何命令,并且可以访问系统中的任何内存位置。上下文切换的时候,进程就处于内核模式。
图42 上下文切换
6.5.7hello的程序执行:
以hello作为一个独立的进程与其他进程并发执行,内核为hello维持一个上下文,在hello的某个时间片内,若内核判断它已经运行了足够长的时间,那么内核可以决定抢占hello进程,并重新开始一个之前被抢占了的进程,并使用上下文切换的机制将控制转移到新的进程,该机制具体执行分为三步:1)保存当前进程的上下文,2)恢复被抢占进程被保存的上下文,3)将控制转移给这个新的进程;这样,内核就完成了对hello与其他进程的调度。
6.6.1异常:
异常可以分为四类:中断(interrupt)、陷阱(trap)、故障(fault)和终止(abort)。其属性列表如下:
类别 |
原因 |
异步/同步 |
返回行为 |
中断 |
来自I/O设备的信号 |
异步 |
总是返回到下一条指令 |
陷阱 |
有意的异常 |
同步 |
总是返回到下一条指令 |
故障 |
潜在可恢复的错误 |
同步 |
可能返回到当前指令 |
终止 |
不可恢复的错误 |
同步 |
不会返回 |
图43中断
图44陷阱
图45故障
图46 终止
6.6.2在hello执行过程中进行测试:
图47 随便乱按
在hello执行过程中随意输入字符和回车并不会对程序本身造成任何影响。由于shell同时只能有一个前台任务,乱按敲出的乱码被认为是命令,所有的输入会被阻塞在缓冲区中,待hello结束后进行处理。
图48 输入Ctrl-Z
进程收到 SIGSTP 信号,hello进程挂起。
图49 输入Ctrl-C
输入Ctrl-z进程收到 SIGINT信号,hello被终止。
图50 运行pstree命令
图51 运行ps、jobs、fg、kill命令
输入ps查看当前进程的状态包括PID
输入jobs查看被挂起的进程hello
输入fg 1 重启hello进程
输入 kill -9 和PID终止进程
再输入ps发现hello已经被杀死
逻辑地址:逻辑地址是指在计算机体系结构中是指应用程序角度看到的内存单元、存储单元、网络主机的地址。逻辑地址往往不同于物理地址,通过地址翻译器或映射函数可以把逻辑地址转化为物理地址。
物理地址:它是在地址总线上,以电子形式存在的,使得数据总线可以访问 主存的某个特定存储单元的内存地址。
CPU发送一条地址,Cache组数的大小要求将PA分为CT(标记位)、CS(组号)以及CO(偏移量)。根据CS寻找到正确的组,比较每一个的标记位是否有效以及CT是否相等,如果命中就直接返回想要的数据,如果未命中,就依次去L2、L3或者主存来判断是否命中:当命中时,将数据传给CPU同时更新各级Cache
本章课程中没有学到
1.hello的一生:当hello.c程序被程序员输入电脑中后,在磁盘中静静存放,当其被执行时,P2P开始了。
2.经过cpp预处理,修改为hello.i文本文件,ccl将hello.i编译为hello.s汇编文件,as将hello.s翻译成机器语言文本指令,最后变成了可重定位目标文件hello.o,ld将hello.o与库函数相互链接形成可执行文件hello。
3.运行hello时,在shell中输入指令,shell用fork函数创建子进程,用execve加载hello,内核通过上下文切换调度各任务的执行,遇到异常或信号触发对应的异常处理程序。最终hello结束一生,系统清除其程序痕迹。实现020
在计算机系统的学习中,我深刻理解了我所在的专业到底是在研究什么东西,处理什么问题,在研究问题的过程中我解决了许多之前的困惑,在hello的一生中我探索到了程序实现最本质的过程,这些都很有趣
中间产物名及其说明
文件名 |
文件作用 |
hello.c |
源文件 |
hello.i |
hello.c经过预处理(cpp)后的文本文件 |
hello.s |
hello.i经过编译(ccl)得到的汇编文件 |
hello.o |
hello.s经过汇编(as)得到的可重定位目标文件 |
hello |
hello.o与其他目标文件链接后得到的可执行目标文件 |
elf.txt |
hello.o的elf文件 |
hello_elf.txt |
hello的elf文件 |
[1] Randal E. Bryant;David R. O’Hallaron. 深入理解计算机系统. 北京:机械工业出版
[2] readelf命令使用说明_木虫下的博客-CSDN博客_readelf
[3] 编译和链接的过程_douguailove的博客-CSDN博客_编译过程