计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 网络空间安全
学 号 2021113276
班 级 2103901
学 生 刘禹航
指 导 教 师
计算机科学与技术学院
2023年5月
本文遍历了hello.c程序在Linux系统下的生命周期,通过gcc、objdump、edb等工具对其代码进行预处理、编译、汇编、链接与反汇编并进行分析,同时对hello的进程运行、内存管理等过程的进行探索,以更深入理解Linux系统下的存储层次结构、异常控制流、虚拟内存等相关内容,更加深入地理解计算机系统课程的知识。
关键词:计算机系统;计算机体系结构;程序的生命周期;深入理解计算机系统
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
- P2P,即program to process。Hello.c文件经过预处理,编译,汇编,链接变为可执行文件,即程序,打开shell输入./hello,shell通过fork产生子进程,hello成为进程,发生了由程序到进程的转变。
- 020,即zero to zero.初始时内存中无hello的有关内容,当hello程序结束后,hello进程被回收,内存中与hello有关的数据被删除,再次变为0,实现了由0到0的变化。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
处理器 11th Gen Intel(R) Core(TM) i7-11370H @ 3.30GHz 3.00 GHz
机带 RAM 16.0 GB (15.7 GB 可用)
系统类型 64 位操作系统, 基于 x64 的处理器
1.3 中间结果
文件名 | 功能 |
hello.c | 源程序 |
hello.i | 预处理后得到的文本文件 |
hello.s | 编译后得到的可重定位目标文件 |
hello.o | 汇编后得到的可重定位目标文件 |
hello.elf | 用readelf读取hello.o得到的elf文件 |
hello.asm | 反汇编hello.o得到的反汇编文件 |
hello1.elf | 由hello可执行文件生成的.elf文件 |
hello1.sam | 反汇编hello可执行文件得到的反汇编文件 |
hello | 可执行文件 |
1.4 本章小结
本章对hello进行了一个总体的概括,首先介绍了P2P、020的意义和过程,介绍了作业中的硬件环境、软件环境和开发工具,最后简述了从.c文件到可执行文件中间经历的过程。
第2章 预处理
2.1 预处理的概念与作用
(1)预处理的概念
预处理步骤是指程序开始运行时,预处理器(cpp,C Pre-Processor,C预处理器)根据以字符#开头的命令,修改原始的C程序的过程,其中 ISO C/C++要求支持的包括#if、 #ifdef、 #ifndef、 #else、 #elif、 #endif(条件编译)、 #define(宏定义)、 #include(源文件包含)、 #line(行控制)、 #error(错误指令)、 #pragma(和实现相关的杂注)以及单独的#(空指令)。除此之外,预处理过程还会删除程序中的注释和多余的空白字符。预处理通常得到另一个以.i作为拓展名的C程序。
(2)预处理的作用
在集成开发环境中,编译,链接是同时完成的。其实,C语言编译器在对源代码编译之前,还需要进一步的处理:预编译。预编译的主要作用如下:
●将源文件中以”include”格式包含的文件复制到编译的源文件中。
●用实际值替换用“#define”定义的字符串。
●根据“#if”后面的条件决定需要编译的代码。
2.2在Ubuntu下预处理的命令
命令:cpp hello.c > hello.i
2.3 Hello的预处理结果解析
打开产生的hello.i文件,可以看到变成了3000多行,新增的内容是预编译产生的,是stdio.h,unistd.h,stdlib.h的展开。
2.4 本章小结
本章介绍了预编译的概念和作用,介绍了几个常见的预编译指令,通过观察预编译后产生的文件演示了预编译的具体效果。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
(1)编译的概念
编译是指C编译器ccl通过词法分析和语法分析,将合法指令翻译成等价汇编代码的过程。
(2)编译的作用
将高级语言源程序翻译成目标程序
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1 数据
(1)常量
①字符串常量
Printf函数中出现的字符串常量"用法: Hello 学号 姓名 秒数!\n"存储在
.rodata只读数据节
②数字常量
存储在.text节中
(3)变量
①全局变量 存储在.data节中
②局部变量 存储在寄存器或栈中
循环条件i=0,可以看出变量i存储在栈中
3.3.2 赋值
对局部变量的赋值由mov指令来完成,根据操作数值大小,后缀分别为b,w,l,q,分别对应1,2,4,8字节。
3.3.3 算术操作
指令 | 效果 |
leaq s,d | d=&s |
add s,d | d=d+s |
sub s,d | d=d-s |
循环变量i的自增操作。
3.3.4 控制转移操作
对应汇编代码
C语言代码标识如果argc!=4执行printf函数,否则不执行。汇编代码使用je条件跳转指令,如果两个数不相等,则跳转到L2位置。
3.3.5 函数操作
在x86-64系统中,可以通过寄存器最多传递6个整型(即整数和指针)参数。第1~6个参数一次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,剩下的参数保存在栈当中。
- main函数
参数传递:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储。
函数返回:设置%eax为0并且返回,对应return 0 。
- printf函数,sleep函数,exit函数,getchar函数
通过call指令实现函数调用
3.4 本章小结
本章主要介绍了编译的概念以及过程,编译是将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备的过程。并且以hello.s文件为例,介绍了编译器如何处理各个数据类型以及各类操作,验证了大部分数据、操作在汇编代码中的实现。
第4章 汇编
4.1 汇编的概念与作用
(1)汇编的概念
汇编是指汇编器(assembler)将以.s结尾的汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并把结果保存在以.o结尾的目标文件中的过程。
(2)汇编的作用
汇编就是将高级语言转化为机器可直接识别执行的代码文件的过程,汇编器将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式。
4.2 在Ubuntu下汇编的命令
命令:gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
输入readelf -a hello.o > hello.elf命令,得到hello.elf文件
- elf头
以 16字节序列 Magic 开始,其描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头大小、目标文件类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量等相关信息。
- 节头
- 重定位节
- 符号表
4.4 Hello.o的结果解析
输入 objdump -d -r hello.o > hello.asm 命令得到反汇编文件
- 操作数的表示
hello.s中用十进制表示;hello.o中用十六进制表示
- 分支转移
hello.s中程序跳转的地址为段名称,如.L2,.L3等。
而hello.asm中跳转地址为具体指令地址,如main+0x32
- 函数调用
hello.s中使用call+函数名的指令来进行函数调用,而hello.asm中call后面是函数指令的地址
4.5 本章小结
本章对汇编的概念与作用进行了介绍。以hello.s文件在Ubuntu系统下通过将hello.s文件汇编为hello.o文件,并生成hello.o的ELF格式文件hello.elf为例,研究了ELF格式文件的具体结构,比较了hello.o的反汇编代码(保存在hello.asm中
)与hello.s的异同。
5.1 链接的概念与作用
(1)链接的概念
链接是指通过链接器(Linker),将程序编码与数据块收集并整理成为一个单一文件,生成完全链接的可执行目标文件的过程。
(2)链接的作用
将为了节省源程序空间而未编入的常用函数文件(如printf.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的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
- elf头
- 节头
- 程序头
- 动态节
- 符号表
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
5.5 链接的重定位过程分析
相比hello.o,hello中增加了hello.c源文件所用到的库函数,如printf、getchar、atoi等的具体实现,hello中添加了节,如.init、.fini等,hello.o中的相对偏移地址到了hello中变成了虚拟内存地址,hello.o中跳转以及函数调用的地址在hello中都被更换成了虚拟内存地址。
链接器在完成符号解析以后,就把代码中的每个符号引用和一个符号定义关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。然后就开始重定位,将合并输入模块,并为每个符号分配运行时的地址。链接器将所有输入到hello中相同类型的节合并为同一类型的新的节,然后链接器将运行时内存地址赋给新的节,赋给输入模块定义的每个节,以及赋给输入模块定义的每一个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。然后是重定位节中的符号引用,链接器会修改hello中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址。
5.6 hello的执行流程
地址 | 子函数名 |
401000 | _init |
401020 | .plt |
401090 | puts@plt |
4010a0 | printf@plt |
4010b0 | getchar@plt |
4010c0 | atoi@plt |
4010d0 | exit@plt |
4010e0 | sleep@plt |
4010f0 | _start |
401120 | _dl_relocate_static_pie |
401125 | main |
4011c0 | _fini |
5.7 Hello的动态链接分析
5.8 本章小结
本章分析了可执行文件hello的ELF格式及其虚拟地址空间,分析了重定位过程、加载以及运行时函数调用顺序以及动态链接过程,深入理解了链接和重定位的过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程的经典定义就是一个执行中的程序的实例。
系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量、以及打开文件描述符的集合。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell是一种交互型的应用级程序。它能够接收用户命令,然后调用相应的应用程序,代表用户运行其他程序。
处理流程:shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,运行对应的程序。
6.3 Hello的fork进程创建过程
创建进程后,在子进程中通过判断pid即fork函数的返回值,判断处于子进程,则会通过execve函数在当前进程的上下文中加载并运行一个新程序。execve加载并运行可执行目标文件,且带参数列表argv和环境变量列表envp。只有当出现错误时,execve才会返回到调用程序。
在execve加载了可执行程序之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,即可执行程序的main函数。此时用户栈已经包含了命令行参数与环境变量,进入main函数后便开始逐步运行程序。
6.4 Hello的execve过程
创建进程后,在子进程中通过判断pid即fork函数的返回值,判断处于子进程,则会通过execve函数在当前进程的上下文中加载并运行一个新程序。execve加载并运行可执行目标文件,且带参数列表argv和环境变量列表envp。只有当出现错误时,execve才会返回到调用程序。
在execve加载了可执行程序之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,即可执行程序的main函数。此时用户栈已经包含了命令行参数与环境变量,进入main函数后便开始逐步运行程序。
6.5 Hello的进程执行
内核为每个进程维持一个上下文,上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。hello程序执行过程中同样存储时间分片,与操作系统的其他进行并发运行。
在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个过程称为调度。在此基础上,hello程序与操作系统其他进程通过操作系统的调度,切换上下文,拥有各自的时间片从而实现并发运行。程序在涉及到一些操作时,例如调用一些系统函数,内核需要将当前状态从用户态切换到核心态,执行结束后再及时改用户态,从而保证系统的安全与稳定。
6.6 hello的异常与信号处理
hello执行过程中可能会出现的异常有:中断、陷阱、故障和终止,可能产生的信号有SIGINT、SIGQUIT、SIGKILL、SIGTERM、SIGALRM、SIGCHLD、SIGSTOP等。处理方式可能是将程序挂起等待下一个信号来临,或终止程序。
- 乱按
(2)按回车
(3)Ctrl+Z
输入命令ps查看后台进程,可见hello进程并未停止。输入fg命令,hello进程继续执行。
(4)Ctrl+C
Ctrl+C后输入命令ps查看后台程序发现没有hello进程,说明Ctrl+C终止了程序。
6.7本章小结
本章介绍了程序在shell执行及进程的相关概念,说明了hello程序在shell中通过fork函数及execve创建新的进程并执行程序的过程,内核的调度及上下文切换等机制。研究了hello执行过程中各种操作可能引发的异常和信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址空间的格式为“段地址:偏移地址”,例如“23:8048000”,可以转换为物理地址:逻辑地址CS:EA = 物理地址CS × 16 + EA。保护模式下以段描述符作为下标,通过在GDT/LDT表获得段地址,段地址加偏移地址得到线性地址。
线性地址空间是指一个非负整数地址的有序集合,例如{0,1,2,3……}。在采用虚拟内存的系统中,CPU从一个有N = 2n个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间。
而对应于物理内存中M个字节的地址空间{0, 1, 2, 3, …, M-1}则称为物理地址空间。
Intel处理器采用段页式存储管理,前者将逻辑地址转换为线性地址从而得到虚拟地址,后者将虚拟地址转换为物理地址。逻辑地址中的偏移地址需要经过段地址到线性地址的转换变为虚拟地址,然后通过MMU转换为物理地址,才能找到对应物理内存。
7.2 Intel逻辑地址到线性地址的变换-段式管理
将程序按内容或过程函数关系分成段,例如程序段、数据段……以段为单位分配内存,通过地址映射机制,将段式虚拟地址转换成实际内存物理地址。段的长度由相应的逻辑信息组的长度决定,因而各段长度不等。逻辑地址得到段号、段内地址,再根据段号和段表求出基址,再由基址+段内地址即可得物理地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理,是将虚拟地址空间划分为一个一个固定大小的块(称作虚页),同一时候也让实际地址空间也划分为一个一个相同大小的页(称作实页)。
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,MMU就必须查阅相应的PTE,这显然造成了巨大的时间开销,为了消除这样的开销,MMU中存在一个关于PTE的小的缓存,称为翻译后备缓冲器(TLB)。TLB通过虚拟地址VPN部分进行索引,分为索引(TLBI)与标记(TLBT)两个部分。这样,MMU在读取PTE时会直接通过TLB,如果不命中再从内存中将PTE复制到TLB。
在以上机制的基础上,如果所使用的仅仅是虚拟地址空间中很小的一部分,那么仍然需要一个与使用较多空间相同的页表,造成了内存的浪费。所以虚拟地址到物理地址的转换过程中还存在多级页表的机制:上一级的页表映射到下一级也表,直到页表映射到虚拟内存,如果下一级内容都未分配,那么页表项则为空,不映射到下一级,也不存在下一级页表,当分配时再创建相应页表,从而节约内存空间
7.5 三级Cache支持下的物理内存访问
首先获取物理地址VA,使用物理地址的CI进行组索引,分别对每路与缓存标记CT进行标志位匹配。若匹配成功且块的标志位为1则命中。然后根据块偏移取出数据并返回。若未到相匹配的块或者标志位为0,则缓存不命中。一级cache向下逐级寻找查询数据。然后向上逐级写入cache。在更新cache的时候,需要判断是否有空闲块。如果有空闲块(即有效位为0)则写入;如果不存在则驱逐一个最近最少使用的块重新写入。
7.6 hello进程fork时的内存映射
shell通过fork为hello创建新进程。当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给hello进程唯一的pid。为了给这个新进程创建虚拟内存,它创建当前进程的mm_struct、区域结构和样表的原样副本。它将两个进程中的每个页面都标记为只读,并将每个进程中的每个区域结构都标记为写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好的和调用fork时的虚拟内存相同。而当这两个进程中任何一个进行写操作时,就会触发一个保护故障。当故障处理程序注意到保护异常是由于进程试图写私有的写时复制区域中的一个页面而引起的,它就会在物理内存中创建这个页面的一个新副本,更新页表条目指向这个新的副本,然后恢复这个页面的可写权限。当故障处理程序返回时,CPU重新执行这个写操作,现在在新创建的页面上这个写操作就可以正常执行了。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
1、删除当前进程虚拟地址的用户部分中的已存在的区域。
2、映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。
3、映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4、设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下 文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
缺页故障是指当指令引用一个虚拟地址,在MMU中查找页表时发现与该地址相对应的物理地址不在内存中就会发生故障。
缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页,如果这个牺牲页被修改过,那么就将它交换出去,换入新的页并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送VA到MMU,这次MMU就能正常翻译VA了。
7.9本章小结
本章主要介绍了hello 的存储器地址空间、intel 的段式管理、hello 的页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理,对hello的存储管理有了较为深入的讲解。
结论
hello一生的经历如下:
- 编写:由编辑器编写出hello.c
- 预处理:将hello.c调用的库合并到hello.i中
- 编译:将hello.i编译成为汇编文件hello.s
- 汇编:将hello.s会汇编为可重定位目标文件hello.o
- 链接:将hello.o与其他可重定位目标文件和动态链接库链接成为可执行目标程序hello
- 开始运行:在shell中输入命令行./hello 学号 姓名 秒数
- 创建子进程:shell进程调用fork为其创建子进程
- 运行程序:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数
- 执行指令:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,执行指令
- 访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址
- 动态申请内存:printf调用malloc向动态内存分配器申请堆中的内存
- 异常控制流:如果运行途中键入ctr-c或ctr-z或进行其他操作,则调用shell的信号处理函数进行相应处理
- 结束:shell父进程回收子进程,内核删除为hello进程创建的所有数据结构
《计算机系统》这门课结合《深入理解计算机系统》这本书向我们介绍了一系列的计算机基础知识,如数据的表示,机器指令,编译系统等知识,详略得当得向我们展示了一幅计算机底层的图景,这门课程为以后的学习打好了基础,使我感觉受益匪浅。
附件
文件名 | 功能 |
hello.c | 源程序 |
hello.i | 预处理后得到的文本文件 |
hello.s | 编译后得到的可重定位目标文件 |
hello.o | 汇编后得到的可重定位目标文件 |
hello.elf | 用readelf读取hello.o得到的elf文件 |
hello.asm | 反汇编hello.o得到的反汇编文件 |
hello1.elf | 由hello可执行文件生成的.elf文件 |
hello1.sam | 反汇编hello可执行文件得到的反汇编文件 |
hello | 可执行文件 |
参考文献
- Randal E.Bryant, David O'Hallaron. 深入理解计算机系统[M]. 机械工业出版社.2018.4
- CSDN博客 Ubuntu系统预处理、编译、汇编、链接指令
[3] 博客园 从汇编层面看函数调用的实现原理
[4] CSDN博客 ELF可重定位目标文件格式