赞
踩
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 网络空间安全
学 号 2021112406
班 级 2103901
学 生 刘晓赟
指 导 教 师 吴锐
计算机科学与技术学院
2023年5月
通过各种工具,结合计算机系统的知识,对hello.c的一生进行分析:预处理、编译、汇编、链接成为可执行文件程序到载入内存、成为进程,再到执行完毕、内存回收。本次大作业以hello.c为源文件。
关键词:计算机系统;预处理;编译;汇编;链接;进程;存储;I/O 管理
目 录
P2P:
hello.c文件要通过编译系统变换为hello可执行程序。首先hello.c文件经过cpp预处理器得到文本文件hello.i,之后经过ccl编译器生成汇编程序hello.s,接着再经过as汇编器生成可重定位目标程序hello.o,最后经过ld链接器,与其它用到的库函数可重定位文件链接,生成可执行程序hello
O2O:
O2O程序并不是一开始就在内存空间中的,也就是一开始为0。OS为hello fork一个子进程,然后在execve执行hello程序,OS会为他开辟一个块虚拟内存,并将程序加载到虚拟内存映射到的物理内存中。当程序执行完,OS回收这一程序,同时为该程序的开辟的内存空间也会被回收,此时又变为0。
Windows10
X64 CPU;2GHz;2G RAM;256GHD Disk 以上1.2.2 软件环境
1.2.2 软件环境
Vmware 17pro
Visual Studio 2022 64位
文件名 | 作用 |
hello.c | 源程序 |
hello.i | 预处理文件 |
hello.s | 汇编文件 |
hello.o | 可重定位目标文件 |
hello | 可执行文件 |
hello_asm.txt | hello的反汇编文件 |
hello_o_astm.txt | hello.o的反汇编文件 |
hello_o.elf | hello.o的ELF格式文件 |
hello.elf | hello的ELF格式文件 |
简单介绍hello的p2p和o2o概念,列出过程文件。
概念:预处理是在将程序源代码转换为目标代码时生成二进制代码之前的过程。它的作用通常包括拆分源代码、删除注释、替换宏和包以及处理各种预编译指令。
作用:有多种预处理指令,主要包括#if(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)等等,预处理可以使源代码在不同的执行环境中被方便地修改、移植和调试,也有利于阅读和模块化程序设计。
图 1 hello.c进行预处理
由图2和图3对比,经过预处理过后,程序由原来的23行扩展到了3091行。而在hello.c中的代码注释已被删去,其声明引用的各种头文件也按顺序被读取使用。
图 2 hello.c和hello.i开头部分
图 3hello.c和hello.i尾部
本章主要介绍了预处理的概念以及作用,并对预处理结果进行了分析。
概念:编译是指使用编译器将用源语言编写的源程序转换为目标程序的过程,即将源语言翻译成计算机可以识别的二进制语言的过程。
作用:将源语言转换为汇编语言,为后续过程做准备。
图 4 编译生成hello.s
3.3.1局部变量int
局部变量会储存在栈中。如图5所示:当进入main函数后,先声明了一个int类型的局部变量,但没有初始化。通过移动栈指针申请空间存储局部变量,运行之后会被释放。
图 5
3.3.2字符串常量
字符串常量在hello.s最开头.rodata的LC0与LC1处已被储存,且标记为只读数据,当在函数中使用这些字符串常量时,将使用字符串对应的地址来防止字符串数据被修改。如图6中展示了第一个printf里的字符串。
图 6
3.3.3赋值操作
主要是数据传送指令(MOV类),如图7展示了for循环i=0的操作。
图 7
3.3.4关系操作
关系操作通过CMP指令与其他指令共同配合完成,图8展示了 “<”条件判断通过cmpl+jle实现。
图 8
3.3.5算术操作
算术指令通常有专门的指令,如inc、dec、sub、imul、xor、or、and等等,图9展示了用add进行加一操作。
图 9
3.3.6数组访问
对数组的访问是通过第一个地址 + 偏移量实现的。图10展示了访问argv[1],argv[2],argv[3]的汇编语言,偏移量分别是8、16、24.
图 10
3.3.7控制跳转
程序中的控制转移主要是指条件、循环等控制程序执行的顺序方式的操作。在汇编语言中,它主要由关系操作和条件跳转等指令组合而成。在图11展示中,通过cmpl+je: argc若等于4,则跳转到.L2,否则顺序执行。
图 11
概念:汇编就是将编译来的汇编指令翻译为机器指令,并以可重定位目标文件的格式保存在二进制文件中。
作用:将汇编指令转为机器可以直接识别的机器指令。
编译命令:gcc -c hello.s -o hello.o
结果如图12所示。
图 12
4.3.1可重定位目标elf格式
指令:readelf -a hello.o > hello_o.elf可得到hello_o.elf,图13较为详细地介绍了elf格式的内容。
4.3.2 ELF头
ELF 头描述文件的整体格式。它从一个 16 字节序列开始,描述生成文件的系统的单词的大小和字节顺序,其余部分包括 ELF 头的大小、目标文件的类型、机器类型、节头部表的文件偏移量以及节头部表中条目的大小和数量, 这可以帮助链接器解析和解释目标文件的信息。
图 13 ELF头
4.3.3节头部表
描述目标文件的节,它描述了不同节的名字、类型、位置和大小。
图 14 节头部表
4.3.4可重定位节和符号表
.rel.text是一个.text节中位置的列表,链接器把这个目标文件和其它文件组合时,需要修改这些位置。.symtab表示符号表,它存放在程序中定义和引用的函数和全局变量的信息。(如图15所示)
图 15 可重定位节和符号表
4.4.1机器指令
命令:objdump -s -d hello.o > hello_o_asm.txt(结果如图16)
图 16
对比hello_o_asm.txt文件和hello.s,差别不大,但反汇编文件有机器指令,由操作码和地址码构成,每条机器指令最前面有指令的相对地址。
图 17 hello.s(左),hello_o_asm.txt(右)
4.4.2函数调用
hello.s文件中,函数调用的具体目的地是用函数名给出的,因为调用的函数还在其它库中,其具体调用的地址无法确定,只有在经过链接后函数调用的准确位置才能确定(如图18)。
图 18 函数调用
4.4.3分支跳转
hello.o中分支跳转目的地是段名给出,汇编后分支跳转是有明确的地址偏移。
图 19 分支跳转对比
本章介绍了汇编的概念和作用,以hello.o程序和反汇编对对比,详细分析汇编前后程序的变化(不同),对产生的可重定位目标文件的理解,同时对函数调用和分支跳转的地址转移(偏移)做了工作,方便链接器的链接。
概念:将源代码文件中的多个目标文件(object file)或库文件(library file)连接成一个可执行文件(executable file)的过程。
作用:链接器在编译后的多个目标文件间进行连接,使得我们的程序能够被执行。同时也可以将符号定义和符号引用进行匹配,减小可执行文件大小,提高代码复用性等。
链接的命令:
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/7/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/7/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello
图 20 链接生成helo
5.3.1可执行文件
命令:readelf -a hello > hello.elf生成hello.elf文件。
图 21 ELF可执行目标文件
5.3.2 ELF头
ELF头记录了记录了文件的类型,版本信息,段表文件偏移、节表文件偏移,程序入口点等信息。其中标识为exec表明为可执行目标文件。
图 22 ELF头
5.3.3节头部表
描述节的类型、大小、位置和名字等等,hello节头部表比hello.o节头部表多了 许多信息
图 23 ELF节头部表
5.3.4 程序头部表
是 ELF 格式文件中的一个数据结构,存储着可执行文件或共享库文件的程序头部信息。每个 ELF 格式文件都有一个程序头部表,而不是单独的程序头部。
图 24 程序头部表
在edb加载hello
图 25 edb加载hello
由5.3中各段的起始地址,可以在data dump中查看相应段。以.text段为例,其在节头部表中的信息为:
图 26 .text在节头部表的信息
由此可得其起始地址为0x4004e0,大小为0x1f2字节,在data dump中找到其对应的内容为:
图 27 .text对应内容
5.5.1 hello和hello.o
命令:objdump –d hello > hello_asm.txt生成hello反汇编文件。比较发现不同之处:
5.5.2重定位过程
重定位项目的格式为:
typedef struct{
long offset; //需要被修改的引用的节偏移
long type:32; //重定位类型
symbol:32; //被修改的引用指向的符号
long addend; //偏移调整量
}Elf64_Rela;
在重定位过程中,链接器根据重定位类型的不同得到不同的便宜调整量,通过节偏移和重定位条目的地址就可以计算出重定位后的地址;最基本的重定位类型有两种,分别是重定位一个使用32位PC相对地址的引用和重定位一个使用32位绝对地址的引用。
子程序名 | 程序地址 |
_init | 0x401000 |
puts@plt | 0x401090 |
printf@plt | 0x4010a0 |
getchar@plt | 0x4010b0 |
atoi@plt | 0x4010c0 |
exit@plt | 0x4010d0 |
sleep@plt | 0x4010e0 |
_start | 0x4010f0 |
_dl_relocate_static_pie | 0x401120 |
main | 0x40112d |
_fini | 0x4011b4 |
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它;GNU编译系统使用延迟绑定,将过程地址的绑定推迟到第一次调用该过程时。
在节头部表中查看GOT的起始地址:
图 28 GOT起始地址
在edb中查看GOT的内容:
图 29 在edbGOT
在dl_init之后再次查看GOT内容:
图 30 dl_init之后的GOT内容
发现0x601008和0x601010两个字节发生变化,出现了两个地址:
0x7f7f256f8170和0x7f7f254e4820,这就是GOT[1]和GOT[2]的地址。
本章介绍了链接的概念与作用,通过查看虚拟地址,对比分析反汇编代码等一系列过程,加深了对重定位、执行流程、动态链接的理解。
概念:进程是计算机中正在执行的程序的实体。一个程序被加载进计算机的内存中时,就形成了它对应的进程。
作用:为操作系统提供了多任务并发处理、进程间通信和同步、稳定性和安全性以及高级程序开发+抽象等方面的支持。
作用:壳(shell)是一种命令语言解释器,它是用户与操作系统之间的接口,通过shell,用户可以向操作系统发出各种命令和指令来执行某些操作或完成某些任务。
处理流程:
hello的fork()创建一个新的进程,称为子进程,在此进程中复制父进程的内容和状态。子进程就像一个全新的进程,拥有自己的进程ID(PID),并在自己的地址空间中复制父进程的代码段、数据段和堆栈段等信息。因此,子进程可以像父进程一样执行代码和访问数据,但它拥有自己独立的内存空间。
当父进程调用fork()时,操作系统会创建一个完全相同的子进程,并将子进程的 PID 返回给父进程。子进程的代码从 fork() 返回的位置开始执行,这样父子进程之间便可以并发运行,其中子进程是父进程的副本。
execve函数在当前进程下的上下文中加载并运行一个新程序。函数声明如下:
int execve(const char *filename, const char *argv[], const char *envp[]);
它加载并运行一个可执行目标文件filename,且带参数列表argv和环境变量列表envp,期间会调用一个加载器,它会将创建内存映像并将可执行文件的片复制到代码段和数据段。之后,加载器会跳转到_start函数的地址点来设置用户栈(图31介绍一种典型的用户栈),最后将控制权交给main函数。
图 31 典型的用户栈
由于系统一般会运行多个进程,所以处理器的物理控制流就会被分成多个逻辑控制流。由于进程间轮流使用处理器,内核为每个进程会维持一个上下文,上下文信息就是内核重新启动一个被抢占的进程所需要而定状态,由一些对象的值组成,包括寄存器、程序计数器、用户栈等。而进城间的上下文切换模式如图32所示。其中,进程执行他的控制流的一部分的每一时间段都叫作“控制流时间片”。
图 32 上下文切换
在hello程序中,上下文切换和进程调度主要发生在sleep函数处,执行sleep前hello进程会处于用户模式并顺序执行,当执行sleep函数以后会被切换到到内核模式进入休眠,加入等待序列;当休眠结束后引起中断并切换到用户模式执行被挂起的程序,直至下次上下文切换。
6.6.1可能出现的异常:
异常可以分为四类:中断、陷阱、故障和终止(如图33)
图 33 异常情况
6.6.2hello执行时的异常
正常情况:执行hello程序时,在hello进程结束后,hello进程会变为僵死进程并向父进程Shell发送SIGCHLD信号,父进程Shell会完成hello进程的回收工作。
如图34在程序执行过程中乱按或输入回车。hello程序运行不受影响,待其运行结束后命令行处理这些回车和字符。
图 34 程序执行时乱按或回车
程序运行时输入Ctrl-C会导致进程立即终止,因为Ctrl-C会发送一个SIGINT信号给hello进程,而SIGINT信号的默认操作是终止进程,因此终止处理程序会立即终止进程。如图35。
图 35 程序执行时按Ctrl+C
进程挂起,接下来执行ps、jobs、pstree、fg、kill命令:
图 36 ps
图 37 jobs
图 38 pstree
图 39 用kill给进程发信号
本章介绍了进程的概念和进程的作用,简述了shell的作用和处理流程,并详细介绍了fork进程创建的过程和execve进程加载的过程,同时分析了hello的进程执行,以及执行过程中遇到的异常和信号及相应处理。通过这一章,我们更深入得认识了hello函数在进程中的表现。
7.1.1逻辑地址
逻辑地址是进程在执行时使用的虚拟地址,是一个虚拟的地址空间。在操作系统内部,逻辑地址会经过内存管理单元(MMU)进行地址映射的转换,映射为实际的物理地址,它由一个段标识符(Segment Selector)加上一个指定段内相对地址的偏移量(offset)组成的。
7.1.2虚拟地址
虚拟地址是由操作系统分配给应用程序使用的地址,其实就是逻辑地址的段内偏移offset。
7.1.3线性地址
线性地址指的是虚拟地址到物理地址变换之间的中间层。CPU将逻辑地址中的段选择器和描述符中的信息结合起来,就可以得到线性地址。
7.1.4物理地址
物理地址是指计算机系统中内存单元的实际地址,是处理器(CPU)直接访问内存单时所使用的地址。与虚拟地址相对应,物理地址是指内存地址在物理上的实际位置,以字节为单位进行编号。
使用段式管理来实现逻辑地址到线性地址的转换。它将线性地址划分为段选择子和段内偏移量两部分,其中段选择子用于索引描述符表,以获取一个描述符,该描述符包含了表述段的信息。描述符主要包括段基址、段限制、段访问权限等信息。根据这些描述符信息,CPU 可以将逻辑地址转换为线性地址。
在页式管理中,线性地址被划分为两部分:页目录项索引和页表项索引。页目录表和页表分别存储页目录项和页表项,每一项包含了一个物理页框的起始地址以及该页的属性。页目录项索引和页表项索引组成了一个两级的查找结构,通过这个结构可以找到相应的物理地址(如图40)。
图 40 页式管理
使用TLB时,首先会查找相应的组索引中是否有相应的条目,若没有则会按照一定策略进行条目替换。
而四级页表将逻辑地址分成四部分:页表索引、页目录索引、页目录表索引和页全局目录偏移量。CPU首先访问页全局目录(PGD)在内存中的地址,根据页全局目录偏移量找到页目录表。在四级页表的规格下,虚拟地址被分为了4个VPN和1个VPO,根据VPN依次访问各级页表就可以找到对应的PPN,进而实现物理地址的寻址。(如图41结合四级页表和TLB)
图 41 结合四级页表和TLB
三级高速缓存(L1、L2和L3)内存组成层次结构,根据存储器距离CPU的远近以及访问速度的快慢,从而提升CPU访问内存的速和效率。当CPU访问内存时,如果目标内存地址位于L1缓存中,则直接从L1中读取数据。如果地址不在L1缓存中,则将请求转发到L2缓存,此过程重复进行,直到数据在L1、L2或L3缓存中被发现为止。如果三级缓存都未找到所需数据,则需从存中检索。
当内存请求被发送到L3缓存时,逻辑类似,直到当数据被检索并保存在L3或L2缓存中时,最终数据将被传输到L1缓存,并交付给CPU(如图42)。
图 42 三级Cache
Linux通过将虚拟内存区域与磁盘上的对象关联起来以初始化这个虚拟内存区域的内容,这个过程称为内存映射。
当fork被hello进程调用后,内核会为新进程创建各种数据,并分配给他一个唯一的PID。它将为两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork函数时存在的虚拟内存相同。当两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,如图43。
图 43 私有的写时复制
运行hello程序在调用fork函数创建新子进程及对应的虚拟内存后,execve加载并运行hello.out需要几个步骤,内存映射示意如图44所示。
图 44 调用execve的内存映射
DRAM缓存不命中称为缺页,若程序运行需要的数据和代码未从磁盘加载到内存,就会发生缺页故障。
缺页过程及中断处理:
图 45 缺页故障和处理
动态内存管理是指在运行程序时,按照程序的需要分配和释放内存空间。C语言中提供了一些动态内存分配函数来实现动态内存管理,其中包括malloc和free等函数。
基本的动态内存管理方法是通过调用malloc函数在堆空间中分配一块连续的内存空间,并将其地址返回给调用函数。程序通过指针变量引用这块内存空间,使用完后则调用free函数将其释放,归还给系统。
动态内存管理策略主要包括内存分配算法和内存回收算法,这两者决定了动态分配内存的过程(如图46动态存储分配)
图 46 动态存储分配
本章主要介绍了hello的存储管理,先介绍了如何由指令的虚拟地址从逻辑地址通过段式管理转化到线性地址,再通过页式管理转化到物理地址。之后介绍具体的访存策略,包括四级页表、TLB和三级Cache等等。最后介绍了hello程序如何处理缺页故障异常和实现动态存储分配管理。
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
(以下格式自行编排,编辑时删除)
(以下格式自行编排,编辑时删除)
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
(以下格式自行编排,编辑时删除)
(第8章1分)
hello的一生:
hello的一生承载着我对计算机系统这门课程三个多月的学习记忆,同时也有些遗憾没有将CSAPP真正学透彻,希望我可以一步一个脚印,继续学习计算机系统领域的知识。
文件名 | 作用 |
hello.c | 源程序 |
hello.i | 预处理文件 |
hello.s | 汇编文件 |
hello.o | 可重定位目标文件 |
hello | 可执行文件 |
hello_asm.txt | hello的反汇编文件 |
hello_o_astm.txt | hello.o的反汇编文件 |
hello_o.elf | hello.o的ELF格式文件 |
hello.elf | hello的ELF格式文件 |
《第7章 链接》
《第8章 异常控制流II-信号(new)》
《第8章 异常控制流I-异常和进程》
《第9章 虚拟内存I-概念与系统》
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。