计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021110975
班 级 2103102
学 生 shy
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年5月
摘 要
本文通过对程序员入门时都会碰到的第一个程序hello.c的一生来展开,介绍了程序在预处理、编译、汇编、链接、进程管理等各个方面的详细过程,更加深入地对所学知识进行了回顾和理解,感受计算机系统各个部分之间的详细分工密切配合,体会计算机科学中的伟大思想。
关键词:计算机系统;预处理;编译,汇编,链接,进程……;
目 录
第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本章小结
结论
附件
参考文献
第1章 概述
1.1 Hello简介
Hello一定是每个程序员入门时碰到的第一个程序。最开始的时候,程序员敲入C语言代码(program),经过预处理后生成hello.i文件,再利用编译器对其进行编译,生成hello.s汇编程序,C语言变为了汇编语言,然后经过汇编器生成可重定位目标文件hello.o(二进制文件),在经过编译器的链接生成可执行文件hello。在运行这个程序时,shell为其生成一个process,然后使用execve加载执行这个程序,至此实现了P2P的过程。在使用execve加载可执行文件hello之前,内存中并不存在hello的相关内容,这是from 0,执行execve之后,将进程映射到虚拟内存空间,hello可执行文件会被加载到内存,开始执行hello中的指令。执行结束后,shell会对该进程进行回收处理,释放其所占的内存并删除其上下文,这就是to 0。这样结合起来,hello便是020。
1.2 环境与工具
X86-64 cpu
Windows 10 64位,Vmvare,Ubuntu 16.04
Dev-C++ 64位;GDB/OBJDUMP;GCC;EDB等
1.3 中间结果
中间结果文件名称 |
文件作用 |
hello.i |
预处理后得到的文本文件 |
hello.s |
汇编后得到的汇编语言文件 |
hello.o |
汇编后得到的可重定位目标文件 |
hello_elf.txt |
hello.o的ELF格式信息 |
hello.asm |
反汇编hello.o的反汇编文件 |
hello_2elf.txt |
hello可执行文件生成的elf文件 |
hello2.asm |
反汇编hello可执行文件得到的反汇编文件 |
hello |
hello可执行程序 |
1.4 本章小结
本章简要介绍了hello的P2P,020的含义,列出了实验环境与工具,以及本实验的一些中间结果。
第2章 预处理
2.1 预处理的概念与作用
概念:在编译之前进行的处理。
作用:1.将源文件中以”include”格式包含的文件复制到编译的源文件中。
2.用实际值替换用“#define”定义的字符串。
3.根据“#if”后面的条件决定需要编译的代码。
2.2在Ubuntu下预处理的命令
在Ubuntu下,使用cpp hello.c hello.i命令对hello.c进行预处理。
结果如下:
图2.2.1 Ubuntu下预处理结果
2.3 Hello的预处理结果解析
打开hello.i文件,看到文件已经由原来的23行变为了3105行,比起原来大幅增加。在文件的最后,我们找到了main函数,这里的main函数仍和原来一样,没有经过修改。之前的三千多行是对我们在hello.c中开头引用的库文件stdio.h,unistd.h和stdlib.h的展开。具体过程为:cpp删除#include指令,并到Ubuntu系统默认的环境变量中寻找头文件。将#define和#include全部被解释和替换掉。除此之外,cpp还会将程序中的注释、空白字符等进行删除。
2.4 本章小结
本章主要介绍了预处理的概念及作用,展示了Ubuntu下预处理的命令及与处理结果,分析了预处理的步骤。
第3章 编译
3.1 编译的概念与作用
概念:将预处理完的.i文件通过一系列词法分析、语法分析和优化之后生成汇编文件。
作用:生成的汇编语言文件每一条语句都对应着一条机器代码,它位为不同的高级语言提供了通用的输出语言,比机器代码更加通俗易懂,又比高级语言更加接近底层。
3.2 在Ubuntu下编译的命令
在Ubuntu下使用gcc -S hello.i -o hello.s对.i文件进行编译,
结果如下:
图3.2.1 Ubuntu下汇编结果
3.3 Hello的编译结果解析
3.3.1数据
(1) argc
main函数的第一个参数argc放在寄存器%edi中,并将其地址压入栈中,利用%rbp加偏移量寻址。
图3.3.1.1 argc存储及寻址
(2) argv[]
main函数的第二个参数是char *类型的argv[],其首地址也被压入了栈中,利用%rbp加偏移量寻址。
图3.3.1.2 argv[]存储及寻址
(3) i
main函数中定义了局部变量i,局部变量是存储在栈中的。利用%rbp寻址。
图3.3.1.3 局部变量i存储及寻址
(4) 4
4是一个立即数,在程序中$4的形式出现。
图3.3.1.4 立即数4存储及寻址
3.3.2 赋值
对局部变量i的赋值是利用MOV指令完成的,根据字长的不同,MOV指令后可以加不同的后缀:b(1字节),w(2字节),l(4字节),q(8字节)。而i被定义为4字节的int型,故使用movl指令对其进行赋值,赋值语句如下:
图3.3.2.1 局部变量i赋值
3.3.3 算术运算
在hello.s中,算术运算指令有以下几种:
(1)subq $32,%rsp 开辟新的栈帧
图3.3.3.1 算术指令1
(2)addq $8,%rax 修改地址偏移量
图3.3.3.2 算术指令2(3)addl $1,-4(%rbp) 对i进行加一操作
图3.3.3.3 算术指令3
3.3.4 关系操作
(1)比较argc与4的大小
图3.3.4.1 关系操作1
(2)比较i与8的大小
图3.3.4.2 关系操作2
3.3.5 控制转移
(1)判断argc与4大小之后,如果相等,则跳转至.L2,控制权交给.L2,不相等则不跳转。
图3.3.5.1 控制转移1
(2)对i进行初始化后,直接跳转至.L3,控制权交给.L3
图3.3.5.2 控制转移2
(3)比较i与8的大小,不相等的话跳转至.L4进入循环,否则继续执行,也就退出了循环。
图3.3.5.3 控制转移3
3.3.6函数操作
利用call调用函数,参数通过寄存器传递,返回值在%rax中。
图3.3.6.1 函数调用
3.4 本章小结
本章介绍了编译的概念及作用。通过对hello.s文件的解析,讨论了编译器处理各种数据类型以及各种操作的方式。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编器(as)将.s 文件翻译成机器语言指令,把这些指令打包成一种叫可重定位目标程序的格式,并将结果保存在目标文件(后缀为.o)中。
作用:将汇编代码转换为机器可以执行的二进制代码。
4.2 在Ubuntu下汇编的命令
在Ubuntu下使用gcc -c hello.s -o hello.o命令进行汇编。结果如下:
图4.2 Ubuntu下汇编
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
1.ELF头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序(小端)。ELF剩下的部分包含帮助链接器语法分析和解释目标文件的信息,包括ELF头的大小6(64字节)、目标文件的类型等。
图4.3.1 ELF头
2.节头部表
节头部表包含文件中出现的各个节的意义,包括节的类型,节的位置(偏移量)和大小等信息。
图4.3.2 节头部表
3.重定位节.rela.data
一个.text节中位置的列表,包含.text接种需要进行重定位的信息,当连接器把这个目标文件和其他文件组合时,需要修改这些位置。
图4.3.3重定位节1
4.重定位节.rela.eh_frame
图4.3.4 重定位节2
5.符号表
符号表保存着重定位程序中符号的定义和引用的信息,所有重定位需要引用的符号都在其中声明。
图4.3.5 符号表
4.4 Hello.o的结果解析
首先,在反汇编出的文件中,汇编指令前增加了机器语言,并且操作数由十进制变为了十六进制。机器语言指令是一种二进制代码,由操作码和操作数两部分组成。操作码规定了指令的操作,是指令中的关键字,不能缺省。操作数表示该指令的操作对象。机器语言与汇编语言一一对应。
接下来是指令的不同:
1. 跳转语句
在hello.s中,跳转语句目标地址是直接用段名称来表示的,如图中的.L2,而在反汇编得到的hello.asm中,跳转的目标为具体的地址,如在下图中使用相对寻址的方法,2b<main+0x2b>。
图4.4.1 hello.s与hello.asm中跳转语句的不同
2.函数调用
在hello.s文件中,call后直接跟着函数名称,而在反汇编程序中,call后面的是下一条指令的地址。这是因为该程序调用的函数是共享库中的函数,最终需要经过链接才能确定函数的运行时地址。对于这一类函数调用,call指令中的相对偏移量暂时全部编码为0,然后在.rela.text节添加重定位条目,等待链接时的进一步确定。
图4.4.2 hello.s与hello.asm中函数调用的不同
3.全局变量的引用
在hello.s文件中,使用段名称+%rip访问rodata(printf中的字符串),而在反汇编得到的hello.asm中,使用0+%rip进行访问,其原因与函数调用类似,rodata中数据地址在运行时才能确定,故访问时也需要重定位。在汇编成为机器语言时,将操作数全部设置为0并添加相应的重定位条目。
图4.4.3 hello.s与hello.asm中全局变量引用的不同
4.5 本章小结
本章介绍了汇编的概念与作用。在Ubuntu下通过实际操作将hello.s文件翻译为hello.o文件,并生成ELF格式文件hello_elf.txt,研究了ELF格式文件的具体结构。介绍了机器语言的组成及其与汇编语言之间的映射关系。通过比较hello.o的反汇编代码hello.asm和hello.s,了解了汇编程序与反汇编程序的不同。本质上是汇编程序与机器代码的不同。
第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。
作用:链接可以使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以将它分解成更小、更好管理的模块,可以独立地修改和编译这些模块,当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
在Ubuntu中使用下图命令进行链接:
图5.2.1 Ubuntu下的链接
5.3 可执行目标文件hello的格式
1.ELF头
ELF头中信息种类与之前的hello.elf无太大区别,以magic开始,包括下面的各种文件信息。但程序头大小和节头数量增加,并且获得了入口地址。
2.节头表
3.程序头
4.动态节
5.符号表
6.重定位节
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。可以看到,虚拟地址空间的起始地址为0x400000。
图5.4.1 hello的虚拟地址空间
从elf文件中看出,.inerp的偏移量为0x200,在edb对应位置我们可以找到:
图5.4.2 .inerp节
经过同样的分析,我们找到.text节和.rodata节,在rodata节中我们还看到了字符串Hello:
图5.4.3 .text节
图5.4.4 .rodata节
5.5 链接的重定位过程分析
首先,在hello.o中多了很多函数,如下图:
图5.5.1 hello中多出的函数
其次,hello.o中的main函数地址从0开始,而hello中main的地址是从0x400582开始的。是已经进行重定位之后的虚拟地址。
图5.5.2 main函数起始地址比较
最后,在hello的main函数中,条件跳转指令和call指令之后均为绝对地址,而hello.o中是相对于main函数起始地址的相对地址。
图5.5.3 call指令和条件跳转指令的不同
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
程序名称 |
程序地址 |
ld-2.27.so!_dl_start |
0x00007ffd7dd66fa0 |
ld-2.27.so!_dl_init |
0x00007fa8e2e8a7d0 |
libc-2.27.so!_libc_start_main |
0x0000000000400574 |
libc-2.27.so!__cxa_atexit |
0x00007fa8e2aaabe7 |
hello!__libc_csu_init |
0x00007fde56987c16 |
libc-2.27.so!_setjmp |
0x00007fde56987c3a |
hello!main |
0x0000000000400582 |
hello!puts@plt |
0x000000000040059e |
hello!exit@plt |
0x00000000004005a8 |
*hello!print@plt |
— |
*hello!sleep@plt |
— |
*hello!getchar@plt |
— |
ld-2.27.so!_dl_runtime_resolve_xsave |
0x7fce8cc4e680 |
ld-2.27.so!_dl_fixup |
0x7fce8cc46df0 |
ld-2.27.so!_dl_lookup_symbol_x |
0x7ce8cc420b0 |
libc-2.27.so!exit |
0x7fce8c889128 |
5.7 Hello的动态链接分析
编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,连接器采用延迟绑定的策略。动态连接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,在GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数,在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。在elf文件中我们可以找到got plt开头地址为0x601000,在edb中找到它:
在调用后,其内容变为:
比较可以得知,0x601008~0x601017之间的内容,对应着全局偏移量表GOT[1]和GOT[2]的内容发生了变化。GOT[1]保存的是指向已经加载的共享库的链表地址。GOT[2]是动态连接器在ld-linux.so模块中的入口。这样,接下来执行程序的过程中,就可以使用过程链接表PLT和全局变量偏移表GOT进行动态链接。
5.8 本章小结
本章介绍了链接的概念及作用,并得到了链接后的hello可执行文件的ELF格式文本hello2_elf.txt,据此分析了hello_elf.txt与hello_elf.text的异同。之后,根据反汇编文件hello2.asm与hello.asm的比较,加深了对重定位与动态链接的理解。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是一个正在运行的程序的实例,系统中的每一个程序都运行在某个进程的上下文中。
作用:给应用程序提供两个关键抽象:1.一个独立的逻辑控制流,提供程序一个假象,好像程序独占处理器。2.一个私有地址空间,提供一个假象,好像程序独占内存系统。
6.2 简述壳Shell-bash的作用与处理流程
Shell是指为使用者提供操作界面的软件(命令解析器)。它接受用户命令,然后调用相应的应用程序。Linux系统中所有的可执行文件都可以作为Shell命令来执行。
处理流程:首先对用户输入的命令进行解析,判断命令是否为内置命令,如果为内置命令,调用内置命令处理函数;如果不是内置命令,就创建一个子进程,将程序在该子进程的上下文中运行。判断为前台程序还是后台程序,如果是前台程序则直接执行并等待执行结束,如果是后台程序则将其放入后台并返回。同时Shell对键盘输入的信号和其他信号有特定的处理。
6.3 Hello的fork进程创建过程
父进程通过fork()函数创建一个子进程。除了PID外,子进程与父进程完全相同。子进程得到与父进程用户虚拟地址空间相同的一份副本,包括代码和数据、堆、共享库以及用户栈。父进程中fork返回值是子进程的PID,而子进程中fork返回0.返回值提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。子进程可以读取父进程打开的任何文件。如果子进程运行结束时,父进程仍然存在,则执行对子进程的回收,否则就由init进程回收子进程。
6.4 Hello的execve过程
调用函数fork创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新程序。execve函数从不返回,,他将删除该进程的代码和地址空间的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序。将私有的区域映射进来,例如打开的文件,代码数据段,然后将公共的区域映射进来。后面加载器跳转到程序的入口点,即设置PC指向_start地址。_start函数最终调用hello中的main函数,这样,便完成了在子进程中的加载。
6.5 Hello的进程执行
在程序运行时,Shell为hello fork了一个子进程,这个子进程与Shell有独立的逻辑控制流。在hello的运行过程中,若hello进程不被抢占,则正常执行;若被抢占,则进入内核模式,进行上下文切换,转入用户模式,调度其他进程。直到当hello调用sleep函数时,为了最大化利用处理器资源,sleep函数会向内核发送请求将hello挂起,并进行上下文切换,进入内核模式切换到其他进程,切换回用户模式运行抢占的进程。与此同时,将和来咯进程从运行队列加入等待队列,由用户模式变成内核模式,并开始计时。当计时结束时,sleep函数返回,触发一个中断,使得hello进程重新被调度,将其重等待队列中移除,并将内核模式转为用户模式,此时hello进程就可以继续执行其逻辑控制流。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
(1)程序正常运行,打印9次信息。
(2)在程序运行时按回车,会出现空行,但仍打印9次并正常结束。但会多出几个命令行。
(3)运行过程中按下Ctrl+C,Shell进程收到SIGINT信号,Shell结束并回收hello进程。
(4)运行过程中按下Ctrl+Z,进程收到SIGSTP信号,提示将hello挂起的信息。
(5)ps命令,可以看到进程仍然存在于后台
(6)jobs命令,发现该进程已停止
(7)pstree命令,将所有进程以树形式显示
(8)fg命令,可使该进程转为前台,继续工作。
(9)kill命令,可将对应进程杀死
6.7本章小结
本章介绍了进程的概念以及作用,熟悉了hello的创建,执行,以及各种信号和异常的情况等,分析了不同信号下hello的执行。
结论
hello程序于源代码“出生”,它的一生经历了如下过程:
·预处理:对hello.c进行预处理,生成hello.i文件
·编译:将预处理完的hello.i文件生成hello.s汇编文件
·汇编:将hello.s文件翻译成机器语言,将结果保存在目标文件hello.o中
·链接:与动态库链接,生成可执行文件hello
hello文件运行时,首先shell通过fork创建子进程,再通过execve将hello加载,为hello分配虚拟地址,并通过四级页表和TLB等结构,将虚拟地址翻译成物理地址,然后根据物理地址进行三级cache支持下的物理内存访问,取出相应信息。在程序运行过程中,还能够接受信号并进行异常处理。当hello程序运行结束后,shell父进程进行hello的回收操作,删除其对应的信息。
通过本次实验,我更加理解了程序是怎么执行的,理解了hello程序如何一步步从p2p,从020。更加感受到了计算机中各个部分相互配合,协调工作,体会到了进程,栈这样的计算机科学的伟大思想。这次的大作业让我感受良多。
附件
列出所有的中间产物的文件名,并予以说明起作用。
中间结果文件名称 |
文件作用 |
hello.i |
预处理后得到的文本文件 |
hello.s |
汇编后得到的汇编语言文件 |
hello.o |
汇编后得到的可重定位目标文件 |
hello_elf.txt |
hello.o的ELF格式信息 |
hello.asm |
反汇编hello.o的反汇编文件 |
hello_2elf.txt |
hello可执行文件生成的elf文件 |
hello2.asm |
反汇编hello可执行文件得到的反汇编文件 |
hello |
hello可执行程序 |
参考文献
[1] Randal E.Bryant David R.O’Hallaron. 深入理解计算机系统(第三版). 机械工业出版社,2016.
[2]Tamp_.一个程序是如何被机器运行起来的?.2018.01.27.https://www.jianshu.com/p/368eb8abbfe4
[3]楠兮兮.计算机系统——异常与信号.2020.10.9https://blog.csdn.net/X1009190387/article/details/108961300
[4]C语言中文网.Shell是什么?1分钟理解Shell的概念!.http://c.biancheng.net/view/706.html
标签:文件,链接,Hello,人生,P2P,进程,hello,3.3 From: https://www.cnblogs.com/shycyf/p/16892052.html