计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 人工智能
学 号 2021113146
班 级 21WL025
学 生 傅一川
指 导 教 师 吴锐
计算机科学与技术学院
2023年5月
预处理、编译、汇编、链接,这是hello作为Program经历的过程。Hello.c作为一个入门级的程序,它的执行包含着计算机系统各个环节的相互配合。本文以linux系统下,hello.c从C语言程序经过预处理、编译、汇编、链接生成可执行目标文件hello的过程,以及通过进程管理、存储管理以及IO管理,分析了hello是如何在计算机系统中执行的。带你结识程序一生中所邂逅的那些“人”;带你见证程序一生中经历的那些事。
关键词:P2P;预处理;编译;汇编;链接;I/O;进程管理;存储管理;
目 录
第1章 概述
1.1 Hello简介
From Program to Process。hello.c。经过预处理器得到修改了的源程序hello.i;接着,编译器将其翻译为汇编程序hello.s;汇编器翻译成机器语言指令,把这些指令打包成可重定位目标程序hello.o;链接器将调用的标准C库中的函数对应的预编译好了的目标文件以某种方式合并到hello.o文件中,得到可执行目标程序hello。系统在执行可执行目标程序时会调用fork函数创建子进程,再用execve加载hello程序,这时,hello就由程序变成了一个进程。
1.1.2 020
From Zero-0 to Zero-0。shell中用fork创建子进程,子进程中execve函数运行hello,映射虚拟内存,进入程序入口后载入物理内存,进入CPU处理。CPU为执行文件hello分配时间片,进行取指、译码、执行等流水线操作。hello执行完毕,hello结束,等待被父进程回收,内核删除hello相关数据。
1.2 环境与工具
i5-1135G7
1.2.2 软件环境
1.2.3 开发工具
gcc edb
1.3 中间结果
图1 中间结果
hello | hello.o经过汇编生成的可执行文件 |
hello.c | 源文件 |
hello.i | hello.c经过预处理生成的文件 |
hello.o | hello.s经过汇编生成的文件 |
hello.s | hello.i经过编译生成的文件 |
4 本章小结
本章主要介绍了hello程序的P2P及020的过程,实验中用到的环境与工具,执行hello程序产生的中间结果。
第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理的概念
预处理器cpp根据以字符#开头的命令,修改原始的c程序。比如hello.c中第1行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入到程序文本中。得到一个以.i为文件扩展名的c程序。
2.1.2 预处理的作用
A宏定义:#define 指令定义一个宏,进行宏替换,#undef 指令删除一个宏定义。
B文件包含:#include处理文件包含,将包含的文件插入到程序文本中。
C条件编译:根据#if,#ifdef,#ifndef,,#elif,#else 和#endif 后面的条件决定需要编译的代码。
2.2在Ubuntu下预处理的命令
图2 预处理命令
2.3 Hello的预处理结果解析
图3-4 hello.i
Hello.i一共有3000多行,代码量大大增加,在文件的末尾发现了main,多出来的内容是头文件展开的结果。stdio.h里有更多的不存在于hello.i的#include内容,还有类似#ifndef的指令。预处理的程序只是将源程序做了一些简单的处理,仍是C语言的源程序,可以正常阅读。
2.4 本章小结
本章主要介绍hello.c程序预处理的概念和作用,同时还通过实际操作对目标文件进行了预处理和结果展示,预处理是hello程序P2P的第一步。Ubuntu下将hello.c文件预处理生成了hello.i文件。
第3章 编译
3.1 编译的概念与作用
3.1.1编译的概念:
编译过程以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。编译是指将便于人编写、阅读、维护的高级计算机语言所写作的源代码程序,翻译为计算机能解读、运行的低阶机器语言的程序。在这里指编译器(cc1)将文本文件hello.i翻译成汇编语言程序hello.s的过程。
3.1.2编译的作用:
编译的作用是将高级计算机语言所写作的源代码程序翻译为优化后的汇编语言程序(低级语言),包括词法分析,语法分析,中间代码,代码优化,目标代码等过程,且编译器可能在这个过程中根据编译选项对程序进行既可以被程序员理解,又更接近机器语言的优化。
3.2 在Ubuntu下编译的命令
图5 编译命令
3.3 Hello的编译结果解析
图6 编译结果
3.3.1 数据
hello.c中的数值常量(全局变量)在hello.s中存在对应,编译器用立即数处理。
hello.c中argc与4比较,hello.s出现立即数$4。
图7-8 数值常量
hello.c中循环条件为i<8,hello.s中小于等于立即数$7。
图9-10 数值常量
hello.c中两个字符串常量(全局变量)“用法: Hello 姓名 学号 秒数!\n”和“Hello %s %s”,编译器将其放入.rodata节。
图11-12 字符串常量
printf时编译器先取出字符串常量存放的地址到%rax再存入%rdi。
图13-14 字符串常量
main定义了局部变量i,局部变量存储在栈里。int型i存储在栈中-4(%rbp)的位置。int argc存放在寄存器%edi中,储存在栈中-20(%rbp)的位置。
图15-16 局部变量
3.3.2 赋值
程序中还出现了对局部变量i的赋值赋初值为0,由 movl $0, -4(%rbp)完成。由movl将4字节的$0赋值栈中储存临时变量的位置。
3.3.3 类型转换
hello.c中,经过atoi(), argv[3]的由char转换为int。
图17 类型转换
3.3.4 算术操作
for循环条件里i++。
图18-19 算术操作
3.3.5 关系操作
判断argc是否为4
图20-21 关系操作
循环时判断i的大小
图22-23 关系操作
这里把i<8的操作转换为了i<=7。意义相同。
3.3.6 数组/指针/结构操作
数组首地址储存在栈里,通过首地址+偏移量访问数组。通过定义char* argc[]用二维的数组来储存字符串,argc[]的每一行保存一个字符串。首先,程序计算-32(%rbp)(argv地址),并存入%rax。argv[2](%rax + 16)存入%rdx。argv[1]存入%rsi。argv[3]存入%rdi。
图24 数组指针结构操作
3.3.7 控制转移
if语句
argc与4比较,若=4,跳转.L2。若!=4,正常执行后面的语句。
图25 if
for循环
循环条件为i<8。给i赋初值0后跳转,开始执行循环。每次循环开始前都比较一次,若i<8,跳转.L4。否则继续执行下面的语句。
图26 for
3.3.8 函数调用
函数调用进行参数传递和返回值。
puts()
hello.c中打印的是printf函数,hello.s里编译器改用puts函数。
图27 puts
exit()
先将$1存入%edi,然后call调用exit函数。
图28 exit
printf()
先进行了三个参数的准备,然后调用了printf函数。
图29 printf
atoi()
argv[3]存入%rdi,然后调用atoi函数。
图30 atoi
sleep()
参数为atoi(argv[3])的返回值存入%edi。
图31 sleep
getchar()
没有参数,直接调用。
图32 getchar
3.4 本章小结
本章介绍编译器将预处理文件转换为汇编语言文件时,处理的数据类型和各种操作。以hello.c和hello.s中的代码为例,解读了数据、赋值、类型转换、算术操作、关系操作、数组/指针/结构操作、控制转移、函数调用等。
第4章 汇编
4.1 汇编的概念与作用
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
汇编器(as)将.s翻译成机器指令,并打包成可重定位目标程序的格式,并将结果保存在.o目标文件(二进制文件)中。
4.2 在Ubuntu下汇编的命令
图33 汇编命令
4.3 可重定位目标elf格式
图34-35 elf
ELF头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助连接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(可文件重定位)、处理器体系结构(x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。
节头部表
节头部表记录了每个节的信息,包括节的名称、地址、类型和偏移量,以及可以对各部分进行的操作权限。本文件没有节组、程序头和动态节。
重定位节
重定位节中包括所有需要重定位的符号的信息(偏移量、信息、类型、符号值和符号名称+加数等)。重定位节包括.rela.text节与.rela.eh_frame节。
其中在类型部分,R_X86_64_PC32是静态链接的内容,R_X86_64_PLT32是动态链接内容。在最后有一个与重定位相关的异常处理单元.rela.eh_frame。
.symtab表
符号表中存放了程序中所定义和引用的的全局变量(不包含局部变量)以及函数的信息。
4.4 Hello.o的结果解析
图36-37 结果解析
1、机器语言的构成
机器语言是计算机能直接理解的语言,完全由二进制数构成。在hello.o的反汇编文件中,使用16进制表示,由多个字节表示,每个字节由两个十六进制数表示。机器语言由操作码、操作数的地址、操作结果的存储地址构成。
2、机器语言与汇编语言的映射
机器语言对应的反汇编的每一条语句由多个字节表示,每一组字节都是一条汇编指令,不过由.o文件生成的反汇编文件与hello.s的汇编语言存在差别。
操作数
hello.s中运用的操作数为十进制的,而生成的反汇编立即数使用16进制表示。
函数调用
hello.s中函数调用call+函数名。hello.o中在call指令调用函数写的是符号重定位之后的地址,最终需要通过动态链接器才能确定函数的运行时执行地址。
分支转移
hello.s中的跳转指令是标签,hello.s中的跳转指令是具体的相对偏移地址,标签在实际机器指令中并不存在。
全局变量
hello.s中调用全局变量仍以.LC0占位符表示。而在反汇编代码中,通过链接时的重定位确定地址,然后将该全局变量地址的值赋值给寄存器。
4.5 本章小结
本章介绍了汇编的过程,将.s文件转换成.o可重定位文件,使用readelf看具体查看hello.o的ELF格式文件以及利用objdump进行反汇编查看汇编代码,将反汇编代码与汇编代码进行比较,加深了对汇编过程的理解。
第5章 链接
5.1 链接的概念与作用
链接的概念
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存中并执行,也就是从.o文件到可执行文件的过程。链接是由链接器的程序自动执行的。链接包含符号解析和重定位两步。链接可以执行于编译时、加载时,运行时。
链接的作用
可以将一个大型的应用程序分解成更小、更好管理的模块,不再需要组织巨大的源文件,可以独立地修改和编译这些模块,当我们改变这些模块中的一个时,只需要简单地重新编译,并重新链接应用,而不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
图38 链接命令
5.3 可执行目标文件hello的格式
图39-40 结果
hello由从.interp到.shstrtab26个段组成,可以看到各段的起始地址和大小。
5.4 hello的虚拟地址空间
图41 edb
图42 elf头
图43 指令的装载地址(可读可写可执行)
图44 编码了字符串常量等数据(只读)
图45 运行时堆(保存全局变量数组,由malloc分配)
图46 用户栈
5.5 链接的重定位过程分析
图47-51 结果分析
含有汇编代码的段增多
增加.init节、.plt节
新增函数
_start和printf/getchar/exit等
函数调用、地址访问
进行了重定位,访问字符串、函数调用变成虚拟地址而不是相对地址。
5.6 hello的执行流程
_dl_start
_dl_init
_start
_libc_start_main
_cxa_atexit
_libc_csu_init
_setjmp
_sigsetjmp
_sigjmp_save
main
(argc!=3时
puts
exit
)
sleep
getchar
_dl_runtime_resolve_xsave
_dl_fixup
_uflow
exit
5.7 Hello的动态链接分析
图52 结果分析
起始地址0x403fe8,大小0x48。在edb中的Data Dump中找到这个地址。发现在dl_init前后,.got.plt的第8到15个字节发生了变化。分别对应GOT[1](动态链接器在解析函数地址时使用的信息)和GOT[2](动态链接器ld-linux.so模块中的入口点)。
5.8 本章小结
本章通过解释链接的概念及作用、分析了可执行文件hello的elf格式,以及hello的虚拟地址空间、重定位过程、执行流程,最后对hello进行动态链接分析,介绍可重定位文件到可执行文件的流程和链接的各个过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念
进程是一个执行中的程序的示例。上下文是由程序正确运行所需的状态(包括通用、目的寄存器的内容、存放在内存中的程序的代码和数据、程序计数器、打开文件描述符等)组成的。系统每个程序都运行在某个进程的上下文中。
进程的作用
在现代系统上运行一个程序时,我们会得到一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断一条接一条地执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象都是通过进程的概念提供给我们的。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash是一个命令处理器,它代表用户运行其他程序。Shell-bash还能从文件中读取命令,这样的文件称为脚本。执行一系列的读/求值步骤。
处理流程
1.Shell打印命令行提示符,读取键盘输入的命令行。
2.从终端读取命令,分析字符串,获取参数。
3.检查第一个命令行参数是否是一个内置命令,若是,立即执行。
4.若不是,fork创建子进程。
5.在子进程中,通过execve加载并运行。
6后台运行用waitpid等待作业终止后返回
7.shell重复上述过程直至用户退出。
6.3 Hello的fork进程创建过程
程序可以调用fork()创建子进程。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。调用成功则返回两个值,子进程返回0,父进程返回子进程ID;否则返回-1。
当在终端中输入“./hello 学号 姓名 秒数”后,终端进行解析。因为这不是内置命令,所以终端调用fork()创建一个子进程。新创建的子进程几乎但不完全与父进程相同。父进程和新创建的子进程之间最大的区别是PID不同。
6.4 Hello的execve过程
创建子进程之后,会调用execve函数。根据命令行的文件名参数,execve函数加载并运行可执行目标文件hello。execve参数包括需要执行的程序(通常是argv[0])、参数argv、环境变量envp。调用成功不返回,否则返回-1。
6.5 Hello的进程执行
内核为每个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
进程时间片
一个进程执行它的控制流的一部分的每一个时间段。
用户态与内核态转换
用户模式的进程不允许执行特殊指令、直接引用地址空间中内核区的代码和数据。内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。需要限制应用程序可执行指令所能访问的地址范围来让处理器安全运行。因此划分了用户态与内核态。
内存为hello分配时间片,在用户态下执行并保存上下文。当hello 执行到 sleep时再次上下文切换,控制交给其他进程,一段时间后再次上下文切换,恢复hello在休眠前的上下文信息,控制权回到 hello 继续执行。遇到循环之后,hello进程会多次进行用户模式和内核模式的转变,程序调用 getchar() , hello 从用户态进入核心态,并再次上下文切换,控制交付给其他进程。最终,内核从其他进程回到 hello 进程,在return后进程结束。
6.6 hello的异常与信号处理
乱按+回车
不影响程序正常执行。因为乱按输入的内容在程序执行结束后才键入命令行。
图53 乱按回车
ctrl-Z
显示Stopped后,进程就被停止了。内核发送信号“SIGTSTP”信号触发中断异常。
图54 ctrlz
ps
打印出了各进程的PID。
图55 ps
pstree
查看进程树之间的关系,同时输出对应的进程pid。
图56 pstree
fg
被挂起在后台的hello进程重新调到前台执行。
图57 fg
kill
kill -9 PID杀死hello进程,ps里hello的PID为57493
图58 kill
ctrl+c
内核发送信号“SIGINT”信号触发中断异常。父进程收到信号后,向子进程发送SIGKILL来强制终止子进程并回收。
图59 ctrlc
6.7本章小结
本章介绍了进程的概念与作用,同时介绍了壳Shell-bash作用与处理流程。明确了hello的fork()创建过程与execve()过程并结合了进程上下文信息、进程时间片的概念,介绍用户态与内核态转换和进程调度的过程。最后说明了hello的异常与信号处理过程,如不停乱按,包括回车。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址
指由程序产生的与段相关的偏移地址部分(hello.o中的内容)。
线性地址
是逻辑地址到物理地址变换之间的中间层。基地址+偏移地址=线性地址。
虚拟地址
即线性地址。
物理地址
是放在寻址总线上的地址。如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。如果是写,电路根据这个地址每位的值就在相应地址的物理内存中放入数据总线上的内容。物理内存是以字节(8位)为单位编址的。MMU利用页表实现虚拟地址与物理地址的映射。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理是把一个程序分成若干个段进行存储,段式管理通过段表进行,它包括段号或段名、段起点、装入位、段的长度等。此外还需要主存占用区域表、主存可用区域表。
图60 段式管理
逻辑地址 = 段标识符+段内偏移量。段标识符由索引号、表指示器和请求者特权级组成。段内偏移量是一个不变的常量,直接送到最后的加法器,用于计算线性地址。TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)。利用段选择符里面的索引号在相应的表里面进行取出段描述符。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页,把内存空间按页的大小划分成片或者页面,然后建立页表来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
图61-62 页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是PTE的缓存,分为索引与标记。MMU在读取PTE时直接通过TLB,不命中再从内存中将PTE复制到TLB。并且层次结构的页表为了压缩空间。Core i7在四级页表层次结构的地址翻译中,虚拟地址被划分为4个VPN和1个VPO。
图63 corei7地址翻译
7.5 三级Cache支持下的物理内存访问
每个行是由一个字节的数据块组成的,还有有效位、标记位。高速缓存的大小C=S×E×B。物理内存访问时先由组索引定位,且要求tag相同且有效。
图64 高速缓存通用组织SEB
7.6 hello进程fork时的内存映射
fork()被调用时,内核创建数据结构,并分配给它唯一的PID(与父进程不同)。并且创建了hello进程的mm_struct 、区域结构和页表的原样副本。当fork在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同但相互独立,每个进程都具有私有的地址空间。
图65 fork
7.7 hello进程execve时的内存映射
2.映射私有区域。
3.映射共享区域。
4.设置程序计数器。
图66 execve
7.8 缺页故障与缺页中断处理
指令引用VA时,MMU查找页表,发现对应PA不在内存中,因此必须从磁盘中取出,是一种常见的故障。
图67 缺页
缺页中断处理
缺页处理程序是系统内核中的代码,缺页处理程序选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去。CPU重新启动引起缺页的指令,这次MMU就能正常翻译VA了。
图68 缺页处理
7.9动态存储分配管理
(以下格式自行编排,编辑时删除)
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
7.10本章小结
本章介绍了hello的存储器地址空间的概念和相关的地址计算方法,介绍段式空间和页面管理的相关知识,虚拟地址转换成物理地址的过程(四级页表、TLB加速、三级cache),讨论了页命中和页不命中的相关操作,再看fork创建进程和execve函数运行hello时的具体过程。
第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本章小结
(以下格式自行编排,编辑时删除)
(第8章1分)
结论
预处理器将包含的头文件添加到源码中进行处理,hello.c被补全为hello.i。编译器编译为汇编语言组成的hello.s。然后汇编器将汇编指令转化为机器指令,得到可重定位的hello.o文件。链接器进行符号解析和重定位,将对hello.o文件和其他文件进行链接,得到可执行的hello程序。我们在bash里输入./hello执行这一程序,bash进程将fork子进程,子进程调用execve()对虚拟内存进行映射来执行程序。hello程序中的main函数结束后,子进程将终止,hello最终被shell父进程回收,内核回收为其创建的所有信息。
通过对hello程序的一生的分析,我了解到原来简单的源代码背后是计算机系统庞大而又复杂的抽象,这门课的学习让我体会到计算机底层执行时的复杂结构,增加了对计算机系统的整体认识。《深入理解计算机系统》一书可以带着我们逐步理解计算机系统,但是这只是一种启蒙,为了更好的学习计算机系统,我们还需要更多的实践。
附件
hello | hello.o经过汇编生成的可执行文件 |
hello.c | 源文件 |
hello.i | hello.c经过预处理生成的文件 |
hello.o | hello.s经过汇编生成的文件 |
hello.s | hello.i经过编译生成的文件 |
参考文献
[1] Randal E.Bryant等.深入理解计算机系统(原书第3版)[M]. 北京:机械工业出版社,2016.7:2.