计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 120L021804
班 级 2003006
学 生 赵卓凡
指 导 教 师 吴锐
计算机科学与技术学院
2021年5月
表面上hello程序的运行平平无奇,但他的背后却是进程管理、指令集、内存分配等无数系统精密而稳定的运作构成的。本文通过对hello的一生的分析,从预处理到编译,汇编到链接,运行载入成为进程,从进程管理,IO管理到进程结束等多角度多方面的,全面的分析了hello的一生,从而加深对计算机系统的了解与体会。
关键词:计算机系统;hello程序;内存管理;IO管理;进程管理;P2P
目 录
6.2 简述壳Shell-bash的作用与处理流程 - 22 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 27 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 27 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 28 -
7.7 hello进程execve时的内存映射 - 29 -
第1章 概述
1.1 Hello简介
P2P:hello.c经过预处理获得.i文件、编译获得.s文件、汇编获得.o文件、链接最终成为可执行目标程序hello,在shell中通过命令fork一个子进程,从程序转化为过程的过程,
020:shell调用子程序加载execve,为其映射虚拟内存,加载物理内存,进入程序开始执行,cpu为其分配时间片,形成逻辑控制流。最后父进程回收进程,内核删除数据的过程。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk
软件环境:Windows10 64位;VirtualBox 20.04.4;Ubuntu 16.04 LTS 64位
开发与调试工具:gcc,vim,edb,readelf,HexEdit
1.3 中间结果
hello.c:源文件
hello.i:预处理后得到的文件
hello.o:汇编后得到的文件
hello.s:编译后得到的文件
hello.elf:hello.o的elf格式
hello1.elf:hello的elf格式
1.4 本章小结
介绍了P2P与020,介绍了所需环境与工具,介绍了所有生成的文件。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。
预处理的作用:
1.宏定义。用一个标识符来表示一个字符串,这个字符串可以是常量、变量或表达式。在宏调用中将用该字符串代换宏名。
2.文件包含。把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。
3.条件编译。允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。
2.2在Ubuntu下预处理的命令
命令:gcc hello.c -E -o hello.i
hello.c截图:
hello.i部分截图:
2.3 Hello的预处理结果解析
预处理后的.i文件打开后发现得到了扩展,从23行扩展到3060行。将原文件中的宏进行宏展开,增加了三个头文件stdio、unistd、stdlib的源码。
2.4 本章小结
本章介绍了预处理的概念与作用,将hello.c进行预处理转成hello.i,并对预处理的结果加以解析。
第3章 编译
3.1 编译的概念与作用
编译的概念:编译指利用编译程序从源语言编写的源程序产生目标程序的过程。
编译的作用:编译把.i转为.s,以高级程序设计语言编写的源程序输入,以汇编语言表示的目标程序作为输出。
3.2 在Ubuntu下编译的命令
命令:gcc hello.i -S -o hello.s
hello.s部分代码:
3.3 Hello的编译结果解析
3.3.1数据类型
3.3.1.1整型
- 对于int i:局部变量i存放在栈上-4(%rbp)中。
- 对于立即数:直接以$形式参与操作。
- 对于int argc:存放在寄存器edi上,并赋给-20(%rbp)
3.3.1.2数组
对于char *argv:存放在寄存器rsi上,并赋给-32(%rbp)
argv中每个单位占8个字节,取相应元素时用如图偏移量。
3.3.1.3字符串
3.3.2赋值操作
把立即数$0通过mov赋值给-4(%rbp)中的int i,占据4个字节,l为后缀。
3.3.3类型转换
将第四个参数放在%rdi,之后调用atoi函数,将字符串化为整型。
3.3.4算术操作
3.3.4.1对于i++操作,通过addl $1,-4(%rbp)把i=0进行不断加一,达到循环目的。
3.3.4.2对于数组,通过addq计算偏移量取出数组内容。
3.3.4.3用subq开辟栈空间。
3.3.5关系操作
3.3.5.1对于argc!=4,将立即数$4与存放在-20(%rbp)中的argc比较
3.3.5.2对于i<8的循环中,每次与7比较,≥7则跳出循环。
3.3.6控制转移
3.3.6.1对于判断argc是否等于4中,如果argc不等于4则输出LC0中的字符串,否则就跳转到L2,形成if。
3.3.6.2对于for(i=0;i<8;i++)的循环,先对-4(%rbp)赋值为立即数$0,跳转到L3与立即数7比较,若小于等于7则跳转到L4中进行一系列操作后对-4(%rbp)执行加一后继续与7比较形成循环,否则弹出。
3.3.7函数操作
3.3.7.1对于main:
3.3.7.2对于printf:
第一个字符串只有一串字符,直接把字符串存放在%rdi中,调用puts函数。
第二个字符串有三个参数,前两个在数组中通过偏移储存在%rdx和%rsi中,第三个字符串储存在%rdi中,再通过printf输出。
3.3.7.3对于exit:参数立即数$1储存在%edi中。
3.3.7.4对于atoi:通过偏移把输入的第四个数储存在%rdi中作为参数。
3.3.7.5对于sleep:sleep函数的参数是atoi函数的返回值,所以将储存在%eax中的返回值转移到储存参数的%edi中。
3.3.7.6对于getchar:直接调用。
3.4 本章小结
本章介绍了编译的概念与作用,与各种汇编代码所对应的c语言的内容。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
汇编的作用:把汇编语言书写的程序翻译成与之等价的机器语言程序的翻译程序,使其链接后被机器识别。
4.2 在Ubuntu下汇编的命令
gcc hello.s -c -o hello.o
4.3 可重定位目标elf格式
readelf -a hello.o >hello.elf
生成.elf文件,以下为.elf文件的部分内容
.elf中的内容:
(1)ELF头:以一个16位的Magic序列开始,描述生成该文件的字大小和字节顺序,剩下部分包括帮助链接器语法分析和解释目标文件的信息,还有节头部表中条目的大小和数量。
- 节头部表:记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
- 重定位表:链接时通过重定位表修改位置信息,以下的分别对应L0、puts、exit、L1、printf、atoi、sleep、getchar的位置信息。
(4)符号表:存放程序中定义的全局变量和函数的信息。name记录目标名称,value记录符号地址,size记录目标大小,type记录目标类型,是函数还是数据,bind表示全局还是本地。
4.4 Hello.o的结果解析
objdump -d -r hello.o > hello.asm
hello.o的内容如下:
与上面的hello.s相比,有以下差别:
- 分支转移:汇编代码的分支转移是通过形如L0、L1等助记符的段名称进行转移,而在机器语言中不通过段名称转移,而是通过确切的地址,表示为主函数加段内偏移。
- 函数调用:汇编代码的call后面直接跟函数的名称,而在反汇编代码中,call后面加由主函数加偏移量构成的下一条指令的地址,因为机器语言中调用的函数在共享库中,无法确定位置,所以相对位置为0,,在重定位表中为其设置偏移量,等待进一步确认。
一一映射关系:每一条汇编代码都可以用二进制机器指令表示,每一条机器指令由操作码和操作数构成,从而建立起一一对应关系。
4.5 本章小结
本章介绍了汇编的概念与作用,分析了ELF文件的内容,生成了.o文件,分析了汇编代码与反汇编代码的差别,分析汇编语言到机器语言一一映射关系。
第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头:type从REL变为EXEC,节点数量变为27个
- 节头部表:
- 重定位表:
- 符号表:
5.4 hello的虚拟地址空间
使用edb加载hello,查看datadump中的信息。从0x400000开始0x400ff0结束。
打开symbols窗口找到各节信息的位置。
比如main在0x401125,在datadump中找到对应位置:
5.5 链接的重定位过程分析
命令:objdump -d -r hello >hello.out
反汇编程序hello.out部分代码:
与hello.o的差别:
- hello反汇编中已经是确定的虚拟地址,而hello.o的反汇编中地址仍是0,未完成重定位。
- 函数调用时,hello.o中call后面接的是下一条指令,不是函数的所在位置,而hello中完成了重定位,call直接指向函数所在的虚拟地址。
- hello.o中只有main,hello中还有很多节,像_init、.plt和各种函数等。
5.6 hello的执行流程
载入:_dl_start、_dl_init
执行:_start、_libc_start_main
运行:_main,_printf,_exit,_sleep,_getchar,_dl_runtime_resolve_xsave,_dl_fixup,_dl_lookup_symbol_x
退出:exit
5.7 Hello的动态链接分析
动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
GOT起始位置在如下404000.
在调用前404008后的16个字节都是0,存放PLT中函数调用下一条指令
调用后变为7f9ba6a2e190、7f9ba6a17ae0两个地址
5.8 本章小结
本章介绍了链接的概念与目的,将hello.o链接成可执行文件hello,了解其虚拟空间,进行重定位过程分析,了解其执行流程和动态链接分析。
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:进程是正在运行的程序的实例
进程的作用:为用户提供了独占处理器和内存的假象。
6.2 简述壳Shell-bash的作用与处理流程
作用:是一个交互型应用级程序,代表用户运行其他程序,接收用户输入的命令并把它送入内核去执行。
处理流程:
(1)从终端读入命令行
(2)切分命令行字符串获得参数
(3)检查第一个命令行参数是否内置,是则执行,不是则fork创建子程序
(4)子程序中调用(2)获取参数,调用execve()执行
(5)前台作业等待作业终止后返回
(6)后台作业shell返回
6.3 Hello的fork进程创建过程
终端程序通过调用fork()函数创建一个子进程,子进程与父进程几乎完全相同且并发独立运行,子进程执行期间父进程等待子进程完成。
在hello中,输入./hello 120L021804 zhao 1,运行程序发现hello不是内置的指令,分割后面字符串,作为参数。用fork创建出子程序,调用参数并执行,hello在子进程中执行至结束。
6.4 Hello的execve过程
evecve的作用是加载并运行一个新程序,且包含相对应的一个带参数的列表argv和环境变量的列表exenvp。为子进程调用fork后,子进程调用execve后,加载并运行一个新的hello程序,删除现有的虚拟内存段,创建新的数据和堆栈,再映射私有区域与公用区域,最后将程序计数器指向hello程序的开始处。
6.5 Hello的进程执行
在用户模式下执行第一个输出hello 120L021804 zhao 1,hello调用sleep后陷入休眠,进入内核模式,内核主动释放进程,将hello从运行队列移出进入等待序列,开始计时,内核上下文切换让其他程序执行,等到计时结束后,内核重新把hello从等待序列移入运行序列,形成逻辑控制流。这运行过程中cpu不断切换上下文,使运行过程切分成时间片,交替使用cpu,完成调度。
6.6 hello的异常与信号处理
中断:来自I/O设备的信号,执行下一条指令
陷阱:有意的异常,执行下一条指令。
故障:可能修复的错误情况,如果能修正则返回命令重新执行,否则终止。
终止:不可恢复的错误,终止程序。
- 正常运行:
- 回车:
- Ctrl-Z,ps:程序未关闭
(4)Ctrl-C:
(5)Ctrl-Z,jobs:
(6)Ctrl-Z,pstree:
- Ctrl-Z,fg:
(8)乱按:
6.7本章小结
了解了进程的概念与作用,了解了shell-bash的概念与作用,了解了hello的fork进程创建过程,hello的execve过程与进程执行,了解了hello的异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:经过编译后出现在汇编代码中的地址,由段标识符和相对位置偏移量组成。
线性地址:逻辑地址到物理地址变换的中间步骤。
物理地址:加载到寄存器中的真实地址,真实物理内存对应的地址。
虚拟地址:与线性地址类似。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由段标识符和相对位置偏移两部分组成。段标识符是一个16位长的字段,称为段选择符。其中前13位是索引号,后三位表示是代码段寄存器还是数据段寄存器还是栈寄存器。可以通过索引号在段描述表中找到一个具体的段描述符,描述了一个段。给出逻辑地址后,通过段选择符中的T1字段确定是全局段描述表还是局部段描述表,之后通过索引号找到具体的段描述符,得到其基地址,再加上相对位置偏移,完成了从逻辑地址到线性地址的变换,即完成了段式管理。
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟地址由虚拟页号和虚拟偏移量组成,物理地址由物理页号和物理偏移量组成。其中虚拟偏移量可直接映射到物理偏移量上。页表是一个页表条目的数组,由有效位和物理页号组成,当虚拟页号转为物理页号时,查询页表通过有效位得知是否虚拟地址缓存在物理内存中,否则缓存在虚拟内存中。若虚拟地址已缓存,则直接将相应页表条目中的物理页号和虚拟偏移量串联就得到了物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
CPU产生虚拟地址,传递给内存管理单元,使用前36位虚拟页号向TLB中匹配。这其中36位虚拟页号被分成四片,每片用作一个页表的偏移量。如果上述匹配命中,则直接将相应物理页号和虚拟偏移量串联得到物理地址,如果匹配失败,内存管理单元向页表查询,通过寄存器确定第一级页表的起始地址通过第一片虚拟页号确定出在第一级页表中的偏移量,查找到页表条目,如果在物理内存中,开始确定第二级页表,以此类推,在第四级页表中查找到物理页号,再与虚拟偏移量组合成物理地址。
7.5 三级Cache支持下的物理内存访问
CPU发送一条虚拟地址,在TLB和四级页表支持下变换成物理地址,将物理地址拆分成标记位、组索引、块偏移三部分。先在L1cache中通过组索引找到所在的组。把所在组中的所有行的标记位和物理地址中的标记位匹配,若匹配成功且行的有效位是1,则匹配成功。在相应行中通过物理地址的块偏移,即取出相应字节,将其返回CPU。若匹配不成功,向L2cache、L3cache、内存中继续匹配直到找到为止。匹配成功后,向上一级返回直至L1cache,若上一级中有空余位置就放到空余位置中,若没有,则驱逐一块内容,将目标块放到被驱逐的块位置上。
7.6 hello进程fork时的内存映射
当fork被调用后,内核为新进程创建数据结构并创建虚拟内存,创建当前进程的mm_struct、区域结构和页表的原样副本。将两个进程中的每个页面都标记为只读,将两个进程中的每个区域结构都标记为私有的写时复制。fork从新进程返回时,虚拟内存与刚创建时相同。
7.7 hello进程execve时的内存映射
execve加载后,新的hello程序代替原先的程序,删除当前虚拟地址中用户部分已存在的区域结构。映射私有区域,为新程序的代码、数据、堆栈创建新的区域结构。映射共享区域,与动态链接到这个程序的共享程序链接,映射到虚拟地址的共享区域中,再设置程序计数器,指向代码入口处。
7.8 缺页故障与缺页中断处理
缺页故障:引用一个虚拟地址时,在内存管理单元中查找页表发现该地址对应的物理地址不在物理内存中,而是在虚拟内存中,而发生的故障。
缺页中断处理:确认出物理内存中的牺牲页,如果已被修改,则放入虚拟内存,调出新的页面,更新内存中的页表条目,返回到原先的进程中,重新执行引起缺页故障的命令,重新在内存管理单元中查找页表,发现造成缺页故障的物理地址已在物理内存中,命中。
7.9动态存储分配管理
动态储存分配管理使用动态内存分配器进行。动态内存分配器维护一个进程的虚拟区域,堆。堆是一个不同大小块的集合,每个块由连续虚拟内存组成,分为已分配和未分配两种。未分配的块会保持状态直到被分配,已分配的块会供程序使用直到被释放。动态内存的分配一般分为显式空闲链表管理和隐式空闲链表管理两种。
7.9.1显式空闲链表管理:堆可以将空闲的块组织成一个双向空闲链表,每个空闲块中包含一个前驱指针和一个后继指针。此外,还需要一个边界标记,便于块合并。此时可以通过地址顺序或进出顺序管理链表,从而管理空闲空间。
7.9.2隐式空闲链表管理:每个块由一个字的头部、有效载荷、和一些其他填充组成,空闲块通过头部的大小字段隐形的连接着,形成隐式空闲链表。遍历时分配器遍历所有块,从而遍历所有空闲块。有三种适配方法,第一种首次适配,从头遍历直到找到适合的空闲块。第二种下一次适配,从上次适配遍历过的块的下一个块开始遍历,减少无用情况。第三种最佳适配,遍历所有块找到最适合、剩余空闲空间最小的块。
7.10本章小结
了解了hello的存储器地址空间,了解了逻辑地址到线性地址的变换-段式管理,了解了Hello的线性地址到物理地址的变换-页式管理,了解了TLB与四级页表支持下的VA到PA的变换,了解了三级Cache支持下的物理内存访问,了解了hello进程fork时的内存映射,了解了hello进程execve时的内存映射,了解了缺页故障与缺页中断处理,了解了动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:将所有IO设备抽象成一个文件,把输入和输出抽象成文件的读写。
设备管理:unix io接口:Linux内核引出的低级接口。
8.2 简述Unix IO接口及其函数
Unix IO接口:
(1)打开文件:一个应用程序要求内核打开相应文件,来访问I/O设备。会返回一个描述符,内核记录该文件所有信息,程序只需要记住该描述符。
(2)linux shell 创建的每个进程开始时都有三个打开的文件:标准输入 、标准输出和标准错误。头文件定义了常量来代替显式的描述符值。
(3)改变文件位置:内核对于每个文件保持一个初始为0的文件位置,该位置标示从头部开始的文件的偏移量。程序可以通过函数修改此文件位置。
(4)读写文件:读操作是从当前文件位置开始,复制相应数量的字节到内存,写操作则是从内存读入相应数量的字节到当前文件位置,然后更新文件位置。
(5)关闭文件:一个应用程序完成对文件访问后,要求内核关闭相应文件,内核会删除期间创建的数据结构,并恢复描述符。
Unix IO函数:
(1)打开函数:int open(char* filename,int flags,mode_t mode)将filename文件转为操作符,mode为访问权限
(2)关闭函数:int close(fd)关闭fd文件,返回操作结果
(3)读函数:ssize_t read(int fd,void *buf,size_t n)从fd文件复制n个字节到buf处
(4)写函数:ssize_t wirte(int fd,const void *buf,size_t n)从buf处复制n个字节给fd文件
8.3 printf的实现分析
先看printf函数:
arg获取第二个参数,即输出时格式化串对应的值。
再看其调用的vsprintf函数:
其生成格式化之后的串,并返回了串的长度
再看write函数:
将栈中参数放入寄存器,ebx是第一个字符的地址,ecx是串的长度
再看syscall:
将字符串中的字节复制到显卡的显存,显存中存储ASCII码,字符显示驱动子程序通过ASCII码在库中找到点阵信息存储到vram中,显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点。于是字符串显示在屏幕上。
8.4 getchar的实现分析
程序调用getchar后,等待用户按键,将输入的字符储存在电脑的缓冲区,等待键入回车。键入回车后,getchar从stdio流中每次读取一个字符,如果输入不止一个字符,则保存在缓冲区中,等待后需getchar调用,直到缓冲区清空后,等待用户按键。
异步异常-键盘中断的处理:按键后,不仅产生该按键的码,还产生一个中断信号,使正在运行的程序中断后,运行一个子程序,读取按键的码并转为ASCII码保存到缓冲区内。都结束后,回到下一条指令。
8.5本章小结
了解了IO设备的管理方法,Unix接口及函数,分析了printf的实现过程,分析了getchar的实现过程
结论
通过逐步体会与分析hello经历的过程,了解了hello这个程序的一生,并加深了对计算机系统的了解与体会,它经过:
- 预处理:由.c文件转为.i文件,将文件中调用的库展开并入文件中。
- 编译:从.i文件生成.s文件,将高级语言转为汇编语言。
- 汇编:从.s文件转为.o文件,机器语言写的可以重定位的文件。
- 链接:将.o文件和动态链接库链接成可执行文件。
- 运行:在shell中输入相应命令与参数
- 创建子进程:shell通过调用fork创建子进程
- 加载:shell通过调用execve将程序加载到子程序中
- 执行命令:CPU分配时间片,使hello生成自己的逻辑控制流
- 访问内存:CPU访问由虚拟内存转化成的物理内存
- 动态内存分配:申请动态内存
- 信号:shell接受程序的异常信号与用户请求
- 终止:父进程回收子进程,内核删除数据结构,完成了hello的一生
我体会到hello程序虽小,只有寥寥几行代码,但就像浮在海面的冰山,其底下是庞大的计算机系统,进程管理、内存分配、指令集等无数系统交织错杂,为无数小小的程序服务,体会到20世纪无数计算机学者为这摩天大厦的构建所付出的天才想象与无比努力。
附件
hello:链接得到的可执行文件
hello.c:源文件
hello.i:预处理后得到的文件
hello.o:汇编后得到的文件
hello.s:编译后得到的文件
hello.elf:hello.o的elf格式
hello1.elf:hello的elf格式
参考文献
- Randal E. Bryant, David R. O'Hallaon. 深入理解计算机系统. 第三版. 北京市:机械工业出版社[M]. 2018: 1-737
- printf 函数实现的深入剖析. [转]printf 函数实现的深入剖析 - Pianistx - 博客园
- Ubuntu系统预处理、编译、汇编、链接指令_spfLinux的博客-CSDN博客