计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021113672
班 级 2103101
学 生 吴海宇
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年5月
本文通过分析hello的一生,介绍了程序从生成到结束的全过程,包括预处理,编译,汇编,链接等,给出了一个程序生命周期的实例
关键词:hello;编译;汇编;链接;
目 录
第1章 概述
1.1 Hello简介
P2P:首先,程序员在Windows或Linux环境下,利用CodeBlocks、vim等编辑器敲入C语言代码(Program),然后利用gcc编译器对C语言程序执行编译命令:hello.c文件先经过预处理器cpp,生成hello.i文件,再经过编译器ccl生成hello.s汇编程序,然后经过汇编器as生成可重定位目标程序hello.o,最后通过链接器ld链接生成可执行文件hello。在Linux终端执行./hello命令,运行该可执行文件(Process)。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows10 64位;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位;
开发工具:Visual Studio 2019 64位;GDB/OBJDUMP;GCC;EDB等
1.3 中间结果
hello.c:源代码文件
hello.i:预处理后生成的文件
hello.s:编译后生成的文件
hello.o:汇编后生成的可重定位目标程序
hello:链接之后生成的可执行程序
hello_o_asm.txt:hello.o的反汇编文件
hello_elf.txt:hello.o的ELF文件
hello_run_asm:hello的反汇编文件
hello_elf2.txt:hello的ELF文件
1.4 本章小结
本章进行了hello程序一生P2P,O2O的简要概述,列出了本实验的硬件环境、软件环境、开发工具和本实验中生成的中间结果文件的名字和作用。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念是:在编译之前进行的处理。C语言的预处理主要有以下三个方面的内容:1. 宏定义;2. 文件包含;3. 条件编译,预处理命令以符号#开头,如#if, #endif, #define, #include等。
预处理的作用:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常以.i作为文件扩展名。
2.2在Ubuntu下预处理的命令
Ubuntu下预处理命令为:
gcc -E hello.c -o hello.i
截图如下:
2.3 Hello的预处理结果解析
hello.c文件共3000多行,可以看出前面绝大部分是将程序中#include后的库函数的代码复制上去。
程序的最后为hello.c中的main函数,可以看出,在3046行之后,和hello.c文件的#include之后的行完全相同,预处理阶段并没有对其做改动。
2.4 本章小结
本章介绍了预处理的概念和作用,同时介绍了Ubuntu环境下预处理的命令,并对预处理生成的hello.i文件进行了简要分析。
第3章 编译
3.1 编译的概念与作用
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
概念:将预处理完的.i文件通过一系列词法分析、语法分析和优化之后生成汇编文件。
作用:生成的汇编文件每条语句都以一种文本格式描述了一条低级机器指令,汇编语言为不同的高级语言的不同编译器提供了通用的输出语言,汇编语言相对于预处理文件更利于机器理解。
3.2 在Ubuntu下编译的命令
gcc -S hello.c -o hello.s
3.3 Hello的编译结果解析
3.3.1 数据
main函数的参数argc存在寄存器%edi中,并将其地址压入栈中,利用%rbp的偏移量来表示,由与立即数4的判断语句可知:
局部变量i存放在-4(%rbp)中,初始化为0,然后进入循环:
3.3.2 操作
1.赋值操作:赋值操作利用mov语句,如 表示 i = 0如果是局部变量不赋初值,在汇编代码里没有体现,只在用到并赋初值时才用寄存器等来存储;如果是全局或静态变量变量不赋初值,则会存放在.bss段;如果是已初始化的全局变量,会存放在.data段。
2.类型转换:在对全局变量sleepsecs进行赋值时进行了隐式类型转换,编译器会直接将转换后的数据送给变量,如3.3.1中图所示。
3.关系操作:利用cmp命令来进行关系操作,执行cmp命令后会设置标志位,然后后续利用对标志位的判断来完成其余操作。
- 数组/指针/结构操作:数组地址存储在栈中,利用栈指针的偏移量来读取。
- 控制转移:利用jmp指令和jxx一系列条件跳转指令来进行转移控制。
- 函数操作:利用call指令来调用函数,将函数需要的参数存放在寄存器中或栈中来进行参数传递,函数调用结束之后,返回值保存在寄存器%rax中。
3.4 本章小结
本章介绍了编译(指由.i文件生成.s文件)的概念和作用以及Ubuntu下进行编译的指令。然后对汇编代码进行解析,分别介绍了C语言中赋值操作、类型转换、关系操作、数组/指针操作、控制转移和函数操作等对应的汇编语言的指令。深入理解了编译生成的汇编代码。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编器(as)将.s 文件翻译成机器语言指令,把这些指令打包成一种叫可重定位目标程序的格式,并将结果保存在目标文件(后缀为.o)中。
作用:将汇编代码转换成真正机器可以读懂的二进制代码。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
在Ubuntu中利用readelf命令来查看ELF,并将其重定位为文本文件
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF剩下的部分包含帮助连接器语法分析和解释目标文件的信息。包括ELF头的大小、目标文件的类型等。
一个典型的ELF可重定位目标文件包含下面几个节:
.txt:已编译程序的机器代码。
.rodata:只读数据。
.data:已初始化的全局和静态局部C变量。
.bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。
.symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
.rel.text:一个.text节中位置的列表。
.rel.data:被模块引用或定义的所有全局变量的重定位信息。
.debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量以及原始的C源文件。
节头部表:
符号表:
可以从符号表中看出符号的大小,类型等信息。如main大小为133字节,类型为FUNC,Bind为GLOBAL,sleepsecs大小为4字节,类型为OBJECT等。
重定位条目常见有两种:
1.R_X86_64_32:重定位绝对引用。重定位时使用一个32位的绝对地址的引用,通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。
2.R_X86_64_PC32:重定位PC相对引用。重定位时使用一个32位PC相对地址的引用。一个PC相对地址就是据程序计数器的当前运行值的偏移量。
通过重定位算法,计算出新的重定位地址。
4.4 Hello.o的结果解析
利用objdump -d -r hello.o对hello.o进行反汇编:
与第三章的hello.s对比,首先在指令前增加了其十六进制表示,即机器语言。其次在操作数上,hello.s中操作数为十进制,而hello.o的反汇编中操作数为十六进制。在条件跳转语句上,hello.o的反汇编文件用的是相对偏移量,而hello.s中是函数名:
在调用函数方面,hello.o的反汇编文件中采用重定向的方式进行跳转,链接时根据重定位条目来获得地址信息。
全局变量的访问上,hello.s文件中对于全局变量的访问为LC0和sleepsecs(%rip),而在反汇编代码中是$0x0和0(%rip)
4.5 本章小结
全局变量的访问上,hello.s文件中对于全局变量的访问为LC0和sleepsecs(%rip),而在反汇编代码中是$0x0和0(%rip)
第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。
作用:链接可以使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以将它分解成更小、更好管理的模块,可以独立地修改和编译这些模块,当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
(以下格式自行编排,编辑时删除)
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
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/9/crtbegin.o /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o hello.o -lc -z relro -o hello
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
首先用命令生成ELF文件
查看各段的起始地址、大小等信息:可重定位条目:
程序头表:
Dynamic section:
符号表:
版本信息:
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
可以看出虚拟地址空间起始地址为0x400000
由elf文件可看出.inerp偏移量为0x2e0,在edb对应位置找到:
同样可以在edb中找到.text段和.rodata 段等:
5.5 链接的重定位过程分析
对hello进行反汇编结果如下:
hello和hello.o相比,首先多了很多经过重定位之后的函数,如_init、puts@plt等,hello.o在.text段之后只有一个main函数;hello.o的地址是从0开始的,是相对地址,而hello的地址是从0x401000(_init的地址)开始的,是已经进行重定位之后的虚拟地址;在hello的main函数中,条件跳转指令和call指令后均为绝对地址,而hello.o中是相对于main函数的相对地址。
链接器完成的两个主要任务:符号解析和重定位。
重定位由两步组成:1. 重定位节和符号定义。 2. 重定位节中的符号引用。
如对puts函数的重定位:
在hello.o反汇编代码中,该行二进制编码为e8 00 00 00 00
addr(text)= 0x401105
refaddr = addr(text)+offset = 0x401126,即引用运行时的地址
addr(r.symbol) = addr(puts) = 0x401080
然后更新该引用,*refptr = (unsigned) (addr(r.symbol) + r.addend - refaddr)
= (unsigned) (0x401080 +(-4) – 0x401126) = (unsigned) (-aa) = ff ff ff 56
将其以小段序填入可得 56 ff ff ff ,与反汇编代码一致。
5.6 hello的执行流程
对hello进行gdb调试,用rbreak命令在每个函数入口处打上断点:
然后执行run和continue命令,查看整个程序进行的所有过程
5.7 Hello的动态链接分析
首先在elf文件中找到.got的地址0x403ff0
在edb找到相应地址处,并且在dl_init处设置断点,分析在dl_init前后该地址附近变化:
dl_init前:
dl_init后:
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,跳转到动态链接器中。每个条目都负责调用一个具体的函数。PLT[[1]]调用系统启动函数 (__libc_start_main)。从PLT[[2]]开始的条目调用用户代码调用的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[0]和GOT[[1]]包含动态链接器在解析函数地址时会使用的信息。GOT[[2]]是动态链接器在ld-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。
5.8 本章小结
本章主要介绍了链接的过程,利用edb、gdb、objdump等工具对链接的ELF文件、虚拟地址空间、重定位过程等进行详细分析,同时对hello程序的执行流程以及动态链接也做了简要介绍。至此,hello程序已经从程序代码转换成了一个可执行文件。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
作用:进程为程序提供了一种假象,程序好像是独占的使用处理器和内存。处理器好像是无间断地一条接一条地执行我们程序中的指令。
6.2 简述壳Shell-bash的作用与处理流程
Shell是指为使用者提供操作界面的软件(命令解析器)。它接受用户命令,然后调用相应的应用程序。Linux系统中所有的可执行文件都可以作为Shell命令来执行。
处理流程:首先对用户输入的命令进行解析,判断命令是否为内置命令,如果为内置命令,调用内置命令处理函数;如果不是内置命令,就创建一个子进程,将程序在该子进程的上下文中运行。判断为前台程序还是后台程序,如果是前台程序则直接执行并等待执行结束,如果是后台程序则将其放入后台并返回。同时Shell对键盘输入的信号和其他信号有特定的处理。
6.3 Hello的fork进程创建过程
Shell是指为使用者提供操作界面的软件(命令解析器)。它接受用户命令,然后调用相应的应用程序。Linux系统中所有的可执行文件都可以作为Shell命令来执行。
处理流程:首先对用户输入的命令进行解析,判断命令是否为内置命令,如果为内置命令,调用内置命令处理函数;如果不是内置命令,就创建一个子进程,将程序在该子进程的上下文中运行。判断为前台程序还是后台程序,如果是前台程序则直接执行并等待执行结束,如果是后台程序则将其放入后台并返回。同时Shell对键盘输入的信号和其他信号有特定的处理。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个程序。
int execve(const char *filename, const char *argv[], const char envp[]);
execve函数加载并运行可执行文件filename,且带参数列表argv和环境变量列表envp,execve调用一次并从不返回。
当main程序开始执行时,用户栈的组织结构如下图所示:
6.5 Hello的进程执行
进程为每个程序提供了一种假象,好像程序在独占的使用处理器。如图每个竖直的条表示一个进程的逻辑控制流的一部分。
一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流称为并发地运行。进程也为每个程序提供一种假象,好像它独占地使用系统地址空间。
处理器通常使用某个控制寄存器中的一个模式位来提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。该寄存器描述了进程当前享有的特权。当设置了模式位时,进程就运行在内核模式中。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存的位置。没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,也不允许直接引用地址空间中内核区的代码和数据。
运行应用程序的代码的进程开始处于用户模式中。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式改为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式改回用户模式。
内核为每个进程维持一个上下文。它由一些对象的值组成,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈等。在进程执行的某些时刻,内核可以决定抢占当前进程,并开始一个先前被抢占的进程,这种决策就叫调度。在内核调度了一个新的进程运行后,它他就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。
上下文切换:1. 保存当前进程的上下文 2. 恢复某个先前被抢占的进程被保存的上下文 3. 将控制权传递给这个新恢复的进程。
在hello中,程序执行sleep函数时,sleep显式请求让调用进程休眠,调度器抢占当前的进程,并且利用上下文切换转移到新进程。Sleep函数结束后,再通过上下文切换返回到hello函数中。
6.6 hello的异常与信号处理
1.不停乱按:
如果乱按的过程中包括回车,那么乱按的内容将会在该程序结束之后作为命令输入。
- Ctrl-C:
在键盘上按下Ctrl-C之后,会导致内核发送一个SIGINT信号到前台进程组中的每个进程,默认情况下结果是终止前台作业。
- Ctrl-Z:
在键盘下按下Ctrl-Z之后,会导致内核发送一个SIGTSTP信号到前台进程组的每个进程,默认情况下结果是停止(挂起)前台作业。
- ps命令:
- jobs命令:
- pstree命令:
- fg命令:
- kill命令:
6.7本章小结
本章介绍了有关进程管理的多个概念。介绍了Shell的作用和处理流程,以及利用fork创建子进程、利用execve加载进程的方法。展示hello程序执行的具体过程,以及异常信号的处理机制。
第7章 hello的存储管理
7.1 hello的存储器地址空间
(以下格式自行编排,编辑时删除)
结合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动态存储分配管理
(以下格式自行编排,编辑时删除)
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
7.10本章小结
(以下格式自行编排,编辑时删除)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
(以下格式自行编排,编辑时删除)
结论
hello程序首先从hello.c的源代码文件开始,依次经过:
·预处理:对hello.c进行预处理,生成hello.i文件
·编译:将预处理完的hello.i文件通过一系列词法分析、语法分析和优化之后生成hello.s汇编文件
·汇编:将hello.s文件翻译成机器语言指令,把这些指令打包成一种叫可重定位目标程序的格式,并将结果保存在目标文件hello.o中
·链接:与动态库链接,生成可执行文件hello
hello文件运行时,首先shell通过fork创建子进程,将hello载入,为hello分配虚拟地址,并通过四级页表和TLB等结构,将虚拟地址翻译成物理地址,然后根据物理地址进行三级cache支持下的物理内存访问,取出相应信息。在程序运行过程中,还能够接受信号并进行异常处理。当hello程序运行结束后,shell父进程进行hello的回收操作,删除其对应的数据结构等信息。
通过本次实验,我感悟到计算机系统设计的精巧以及严谨性,每一种机制都能够恰到好处地完成应该完成的功能,并且消耗尽可能少的资源。几乎任何一种异常情况都能够得到合适地处理。可以说我们能有如今的计算机编程环境以及各种基于计算机的应用,都离不开这些底层系统的设计与实现。
附件
hello.c:源代码文件
hello.i:预处理后生成的文件
hello.s:编译后生成的文件
hello.o:汇编后生成的可重定位目标程序
hello:链接之后生成的可执行程序
hello_o_asm.txt:hello.o的反汇编文件
hello_elf.txt:hello.o的ELF文件
hello_run_asm:hello的反汇编文件
hello_elf2.txt:hello的ELF文件
参考文献
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.