计算机系统
大作业
题 目 程序人生-Hello’s P2P
计算机科学与技术学院
2023年4月
关键词:预处理、编译、汇编、链接、可执行文件、进程管理、存储管理、I/O
以hello程序从C文件如何转变为可执行文件的全过程为例,包括预处理、编译、汇编、链接阶段,如何对旧文件进行操作形成新文件的过程。深入了解hello进程在shell执行的过程,进程管理、存储管理的过程,I/O处理的过程。节奏这些过程的分析,了解程序与计算机的配合,深化对计算机系统的理解。
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 5 -
3.2 在Ubuntu下编译的命令............................................................................. - 6 -
4.2 在Ubuntu下汇编的命令............................................................................. - 7 -
5.2 在Ubuntu下链接的命令............................................................................. - 8 -
5.3 可执行目标文件hello的格式.................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 10 -
6.3 Hello的fork进程创建过程..................................................................... - 10 -
6.6 hello的异常与信号处理............................................................................ - 10 -
7.1 hello的存储器地址空间............................................................................ - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................ - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理....................................... - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换............................................. - 11 -
7.5 三级Cache支持下的物理内存访问.......................................................... - 11 -
7.6 hello进程fork时的内存映射.................................................................. - 11 -
7.7 hello进程execve时的内存映射.............................................................. - 11 -
7.8 缺页故障与缺页中断处理........................................................................... - 11 -
8.1 Linux的IO设备管理方法.......................................................................... - 13 -
8.2 简述Unix IO接口及其函数....................................................................... - 13 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
一、P2P:预处理器(cpp)将源程序文本hello.c根据以#开头的命令,转换成修改了的源程序(仍为文本文件)hello.i,接着编译器(ccl)将文本文件hello.i翻译成一个包含汇编语言程序的文本文件hello.s,接下来汇编器(as)将hello.s翻译成机器语言指令,指令打包成可重定位目标程序格式并保存在目标文件hello.o中(二进制文本),最后由链接器将多个可重定位目标程序(例如printf.o)与hello.o合成一个可执行文件hello,并存放在磁盘上。
然后为了在Unix系统上运行,将文件名输入到shall命令行解释器中,fork一个子进程,并用execve运行程序,最终生成一个进程。
二、020:shell对其子进程调用execve函数,然后映射出虚拟内存(为每个进程提供一致的地址空间并保护其不被其他进程破坏的功能),并在程序开始运行时载入物理内存,运行中CPU为程序分配时间片执行逻辑控制流,程序结束时,父进程回收并释放子进程的程序数据与空间占用,最终实现程序执行的全过程。
1.2 环境与工具
硬件环境:X64 CPU;16CPUs;3.2GHz;16384MB RAM;BIOS J6CN40WW
软件环境:Windows11 64位;Codeblocks;Ubantu 16.04 LTS 64位
开发与测试工具:gcc,vim,edb/gdb,readelf等
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c:程序源代码
hello.i:预处理之后得到的修改了的文本文件
hello.s:编译器编译后得到的汇编语言文本文件
hello.o:汇编器生成的可重定位目标文件(二进制文件)
hello: 链接得到的可执行目标文件(二进制文件)
hello_elf1:可重定位文件生成的elf格式文件
hello_elf2:可执行文件生成的elf格式文件
asm1.txt: 可重定位文件反汇编生成的汇编语言文本文件
asm2.txt: 可执行文件反汇编生成的汇编语言文本文件
1.4 本章小结
介绍了hello程序生成到运行的过程,了解实验环境及实验所需生成的文件,介绍了hello的p2p和020的过程和程序执行时的中间结果。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:
预处理(预编译),是预处理器(cpp)以字符#开头的命令,修改原始的C程序,最终生成.i文件的过程。
预处理的作用:
预处理的主要作用是将头文件中的内容插入(“include”格式包含的文件)到源文件中同时根据宏定义在源程序中进行替换(用“#define”定义的字符串)不带参数的宏定义就是在作用区间内简单的字符串替换,而带参数的宏定义还要在此基础上进行参数的替换。删除注释。
2.2在Ubuntu下预处理的命令
在命令行中输入gcc -E -o hello.i hello.c实现预处理
2.3 Hello的预处理结果解析
Hello.c 文件:
Hello.i:
查看hello.i的信息,发现在hello.i文件中把宏定义展开成extern FILE *()形式
将声明内容加入到源文件,并且注释内容已经被删除。
2.4 本章小结
介绍了预处理阶段的概念和作用,对hello.c程序进行了预处理,通过hello.i深入了解了预处理的结果和好处,探究了预处理阶段的机制。
第3章 编译
3.1 编译的概念与作用
概念:编译器(ccl)将预处理后的文本文件.i文件翻译成文本文件.s文件,即转化为汇编指令的过程,汇编指令可以将逻辑一致的代码转化为统一形式。编译过程中可以进行语法的检查、中间代码的生成、源程序的优化。
作用:把高级语言程序翻译成便于机器理解的低级机器指令同时编译时一些指令可以被简化和优化。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
输入gcc -S hello.i -o hello.s对hello.i进行编译得到汇编语言文件hello.s ,生成的hello.s文件如下:
3.3.1 数据
1、全局变量:
.globl :声明全局变量,这两个全局变量分别是字符串"用法: Hello 学号 姓名 秒数!\n"和"Hello %s %s\n"
argc:存储-20(%rbp) argv[]:在-32(%rbp)。 i:在-4(%rbp)在每次循环中更新。
2、立即数:常量表示形式为$数字:
3、局部变量:局部变量存储在寄存器中,局部变量不够被寄存器存放时将其存放在栈中。
3.3.2赋值:
赋值通过movb、movw movl movq 来实现。
hello.c中的i=0在 hello.s中的部分利用movl,把0赋值给i。
3.3.3算数运算:
hello.c中的i++在hello.s中,利用addl,实现i的增加。
3.3.4 条件转移
hello.c中比较argc!=4并转移在 hello.s中:
hello.c比较i<5并跳转,在hello.s是:
3.3.5数组/指针/结构操作
对于数组/指针/结构的操作是通过基址地址偏移寻址的方式存取,偏移量的大小基于对应变量大小。
argc[]通过首地址+偏移量访问
addq $16,%rax解释为argc[2]
addq $8,%rax解释为argc[1]
addq $24,%rax解释为argc[3]
3.3.6 函数操作
main函数
参数argc存储在%edi;argv[]存储在%rsi。将存储返回值的寄存器%eax赋值为0,然后再返回。
printf函数 printf("Hello %s %s\n",argv[1],argv[2]);
调用函数
将LCO的地址传递给%rdi
Exit函数
参数传递:把1通过movl指令传递给%edi
Atoi函数
运用atoi函数按位操作转换类型,将%rax中的值传入%rdi中然后使用call指令调用函数,返回值被传入sleep函数中
Sleep函数;
将atoi的返回值传入%rdi中,使用call指令调用函数。
3.4 本章小结
本章介绍了编译的概念与作用。将hello.i编译成hello.s,并且对编译结果进行各个方面的分析,详细地了解了编译的过程和形式。
第4章 汇编
4.1 汇编的概念与作用
概念:把汇编语言翻译成机器语言的过程,汇编的过程中把机器语言指令生成可重定位目标程序,保存在hello.o中。
作用:汇编将汇编代码翻译成机器可以理解的语言,用于生成可执行文件。
4.2 在Ubuntu下汇编的命令
gcc -m64 -no-pie -fno-PIC -c -o hello.o hello.s
4.3 可重定位目标elf格式
readelf -a hello.o >helloref.txt
文件如图所示:
1、ELF头
ELF头以一个16字节的序列开始的,这个序列描述了生成该文件的系统的字的大小和字节顺序。头以下的部分帮助链接器语法分析和解释目标文件信息。
2、节头
不同节的位置和大小是由节头部表描述
3、程序头
没有
4、重定位节:
重定位的节记录引用外部变量的信息
4.4 Hello.o的结果解析
指令:objdump -d -r hello.o > helloob.txt
反汇编文件中不仅有汇编语言还有机器语言,我们可以发现一条汇编语言的实现对应这相应的机器语言,两者可以相互转化。
数据:hello.s是十进制数而反汇编是16进制数。
函数调用:hello.s使用call指令调用函数,函数名称为目标,而在反汇编文件中函数调用是用下一条指令的相对地址偏移量给出的。
全局变量:hello.s是将全局变量值直接赋给寄存器(使用$.LC0、$.LC1给%edi赋值)但是在反汇编代码中,将可重定位文件重定位之后计算出全局变量的地址,赋值给寄存器。
分支转移:hello.s中的跳转指令跳转到代码内部。而反汇编中的跳转指令跳转到具体的存储地址。
4.5 本章小结
了解了汇编这一过程的概念与作用, 将hello.s汇编得到hello.o文件,同时得到hello.o的ELF可重定位目标文件hello_o_elf.txt,和反汇编得到文件。通过对比分析,深入地探究了汇编的全过程。
第5章 链接
5.1 链接的概念与作用
概念:链接是指将一个或多个由编译器或汇编器生成的目标文件外加库链接为一个可执行文件,即生成可执行文件的过程。
作用:作用是将.o文件与其他可重定位文件组合共同生成可执行文件,并到内存运行。
5.2 在Ubuntu下链接的命令
ld -o hello -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 hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
5.3 可执行目标文件hello的格式
readelf -a hello > hello1.elf
- ELF头
![]() |
- 节头
程序头:
程序头部表用于描述一种映射关系,可执行文件的连续片被映射到连续的内存段。
Dynamic section:在偏移量为0x2e50的位置,包含21个条目。它是可执行文件新增的内容。
重定位节:可执行文件动态链接时还会需要重定位信息。
符号表:符号表有51个条目
5.4 hello的虚拟地址空间
程序在0x400000~0x401000段中载入。并且从0x400000开始,顺序与hello的elf格式文件顺序相同。
5.5 链接的重定位过程分析
指令:objdump -d -r hello
链接器完成了符号解析,就把代码中的每个符号和一个符号的定义关联,此时知道代码节数据节的确切的大小,链接器开始将所有相同类型的节合并为同一类型的新的聚合节,然后把代码节和数据节中对每个符号的引用修改,指向正确的地址,这一过程依赖于重定位条目基于汇编器遇到的最终位置未知的目标引用时生成的。
5.6 hello的执行流程
执行main、printf、_exit、_sleep、_getchar;最后退出。
start 0x4010f0
libc_start_main 0x2f12271d
main 0x401125
printf 0x401040
exit 0x401070
sleep 0x401080
getchar 0x401050
5.7 Hello的动态链接分析
GOT[1]保存的是指向已经加载的共享库的链表地址。GOT[2]是动态链接器在ld-linux.so模块中的入口。过程链接表PLT和全局偏移量表GOT进行动态链接。GOT采用延迟绑定的策略。GOT中保存着下一条指令的地址。dl_init执行后,通过重定位确定函数地址。
查看hello_elf.txt文件可以得到GOT的起始位置为0x403ff0。再用edb查看.got的内容。
比较可知,0x403ff0之后发生了变化,即全局偏移量表GOT[1]和GOT[2]的内容发生了变化。
5.8 本章小结
介绍了链接的过程。以hello.o链接后形成了hello可执行文件为例,对可执行文件的格式进行分析、hello的虚拟地址空间、hello的动态链接分析并通过反汇编hello文件,将其与hello.o反汇编文件进行了对比,详细了解了重定位过程。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是操作系统对正在运行的程序的一种抽象,进程的定义是具有独立功能的一个程序关于数据集合的一次运行活动。进程会提供一种系统上好像只有某一个程序在运行,而且该程序看上去是独占地使用处理器、主存、I/O设备,该程序的代码和数据是系统内存中唯一的对象的假象。
作用:进程提供程序独占地使用处理器并且可以独占内存的假象。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell-bash读取用户命令行,串联用户与内核,可以通过该界面访问操作系统内核。
处理流程:
- 从命令行中读取用户输入
- 解析命令行,检查第一个参数是否是一个内置的shell命令。
3、如果是内置命令,立即解释,如果不是,创建子进程,执行程序。
6.3 Hello的fork进程创建过程
在终端输入./+可执行文件名,shell检查该命令是否为内置命令。判断出其为可执行文件后,shell将会调用fork创建一个新的子进程,然后在这个子进程中调用execve函数进入要执行的程序。
6.4 Hello的execve过程
调用函数fork创建新的子进程,会调用execve函数运行可执行目标文件hello。如果有错误,execve返回到调用程序。首先对可执行文件进行检查,删除当前用户虚拟地址中的已存在区域结构,并将堆栈初始化为0,为代码、数据和栈区域映射创建私有区域,并在写入时进行复制。设置程序计数器,下次运行时能从代码区域入口进入。
6.5 Hello的进程执行
shell接收到可执行文件的执行命令,fork函数创建一个子进程调用execve函数在现在进程的上下文中运行刚才的可执行文件。删除子进程现有的虚拟内存,并创建一块新的空间。然后,通过将虚拟地址空间中的页映射到可执行文件的页,代码段和数据段都会被设置为可执行文件的内容。运行调用printf函数,进程变成内核模式,在printf函数执行完后返回到用户模式,然后调用sleep函数,内核进行上下文切换,调用其他进程运行,计数器记录休眠时间,计时器计到1s时,系统发生中断转换到hello进程原先运行的位置,将先前记录的进程重新放入运行队列中执行。之后调用getchar函数,进入内核模式,再进行上下文切换,运行其它进程,当数据传输结束,发生中断,重新回到hello进程,最后返回终止运行。
6.6 hello的异常与信号处理
中断
中断是处理器外部的I/O设备的信号的结果,中断处理程序运行,进行到下一条指令。
陷阱
陷阱是有意的异常,是执行一条指令的结果,返回到下一条指令。
故障
故障是由错误情况引起的,当故障发生时,是利器将控制转移给故障处理程序,如果错误情况可以修正,则将控制返回到引起故障指令,重新执行,否则处理程序返回到内核abort,终止故障的应用程序。
空格:进程不会被打断。
回车:进程回收。
输入Ctrl C:终止并回收hello进程。
输入Ctrl z:显示提示信息并挂起hello进程
使用ps可以查看进程的基本信息
使用jobs:查看进程状态
使用pstree:可以查看进程树
使用fg:重新运行
6.7本章小结
介绍了hello的进程管理。了解了进程的概念和应用,然后查询了shell-bash的作用与处理流程,fork进程创建过程和execve函数运行过程,hello的执行,hello的异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:汇编程序中地址。逻辑地址由选择符和偏移量组成。
虚拟地址:CPU通过虚拟地址来访问主存,这个虚拟地址在被送到主存前先转换成适当的物理地址,并与物理地址一一对应。
物理地址:在存储器里以字节为单位存储信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址。
线性地址:逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理:是指将一个程序分为若干个段进行储存和管理,每个段都是一个实体。8086共设计了20位宽的地址总线,通过将段寄存器左移4位加上偏移地址得到20位地址,这个地址就是逻辑地址。将内存分为不同的段,段有段寄存器对应,段寄存器有一个栈寄存器、一个代码寄存器、两个数据寄存器。
7.3 Hello的线性地址到物理地址的变换-页式管理
系统将每个段分割成虚拟页的大小固定的快来作为数据传输单元,虚拟内存系统中MMU负责地址翻译,MMU使用存放在物理内存中的被称为页表的数据结构将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB:每次CPU产生一个虚拟地址,MMU就必须查阅相应的PTE,每次都必然造成缓存不命中等一系列时间开销,为了消除这样的开销,MMU中存在一个全相联高速缓存,称为TLB。
四级页表:如果只需要使用的虚拟内存很小,但仍然需要一个十分巨大的页表,造成了内存的浪费,或者在某些情况页表会变得十分巨大。所以虚拟地址到物理地址的转换过程中还存在多级页表的机制:上一级的页表映射到下一级页表,直到页表映射到虚拟内存。
7.5 三级Cache支持下的物理内存访问
物理地址被分为CT(标记)+CL(索引)+CO(偏移量),然后到1级cache里去找对应的标记位为有效的。若找不到再到2级,3级cache里去找对应的字节,找到后返回,若找不到,则启动替换策略,将新的条目逐级替换插入。
7.6 hello进程fork时的内存映射
父进程创建了自己的儿子进程之后,内核为新生儿进程分配唯一一个PID用以标识身份,然后子进程继承了与父进程当前完全一致的在虚拟内存中的副本
7.7 hello进程execve时的内存映射
将.text,.data,.bss都加载到虚拟地址空间之中。
7.8 缺页故障与缺页中断处理
若未分配状态则发生了缺页,先要根据牺牲策略决定一个牺牲页,然后进行页替换。
7.9动态存储分配管理
基本方法与策略:通过维护虚拟内存(堆),一种是隐式空闲链表,一种是显式空闲链表。显式空闲链表法是malloc(size_t size)每次声明内存空间都保证至少分配size_t大小的内存,双字对齐,每次必须从空闲块中分配空间,在申请空间时将空闲的空间碎片合并,以尽量减少浪费。
7.10本章小结
虚拟内存使得程序的调用更加私密,也使得物理空间得到了最大利用。也使得每个进程都有一个自己的虚拟空间,让进程运行的更加效率。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux中的I/O设备被表示为文件,输入输出用文件的读和写来执行
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
打开文件:int open(char *filename, int flags, mode_t mode);
关闭文件:int close(int fd);
读文件: ssize_t read(int fd, void *buf, size_t n);
写文件: ssize_t write(int fd, const void *buf, size_t n);
8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
int printf(const char fmt, …)
{
int i;
char buf[256];
va_list arg = (va_list)((char)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
int getchar(void)
{
char c;
return (read(0,&c,1)==1)?(unsigned char)c:EOF
}
8.5本章小结
本章简述了Linux的I/O设备管理机制,Unix I/O接口及函数,并简要分析了printf函数和getchar函数的实现
结论
回顾hello运行,预处理器将.c文件中的宏定义与库先一步处理好生成.i文件,编译器将预处理完的源码转换成了汇编代码,完成了高级语言向低级语言的转化,
汇编器将汇编代码转化为二进制机器代码。链接器将上述生成的可重定位文件链接后生成可执行文件。在shell中运行Hello,先fork后execve,访问内存以及可能出现的异常处理后Hello真正的运行了。
hello.c 源文件
hello.i 预处理后的文件
hello.s 编译后的文件
hello.o 可重定位文件
hello.elf可重定位文件的ELF
hello 连接后的可执行文件
hellol.elf 可执行文件的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.