目录
第1章 概述................................................... - 4 -
1.1 Hello简介............................................ - 4 -
1.2 环境与工具........................................... - 4 -
1.3 中间结果............................................... - 4 -
1.4 本章小结............................................... - 4 -
第2章 预处理............................................... - 5 -
2.1 预处理的概念与作用........................... - 5 -
2.2在Ubuntu下预处理的命令................ - 5 -
2.3 Hello的预处理结果解析.................... - 5 -
2.4 本章小结............................................... - 5 -
第3章 编译................................................... - 6 -
3.1 编译的概念与作用............................... - 6 -
3.2 在Ubuntu下编译的命令.................... - 6 -
3.3 Hello的编译结果解析........................ - 6 -
3.4 本章小结............................................... - 6 -
第4章 汇编................................................... - 7 -
4.1 汇编的概念与作用............................... - 7 -
4.2 在Ubuntu下汇编的命令.................... - 7 -
4.3 可重定位目标elf格式........................ - 7 -
4.4 Hello.o的结果解析............................. - 7 -
4.5 本章小结............................................... - 7 -
第5章 链接................................................... - 8 -
5.1 链接的概念与作用............................... - 8 -
5.2 在Ubuntu下链接的命令.................... - 8 -
5.3 可执行目标文件hello的格式........... - 8 -
5.4 hello的虚拟地址空间......................... - 8 -
5.5 链接的重定位过程分析....................... - 8 -
5.6 hello的执行流程................................. - 8 -
5.7 Hello的动态链接分析........................ - 8 -
5.8 本章小结............................................... - 9 -
第6章 hello进程管理.......................... - 10 -
6.1 进程的概念与作用............................. - 10 -
6.2 简述壳Shell-bash的作用与处理流程.. - 10 -
6.3 Hello的fork进程创建过程............ - 10 -
6.4 Hello的execve过程........................ - 10 -
6.5 Hello的进程执行.............................. - 10 -
6.6 hello的异常与信号处理................... - 10 -
6.7本章小结.............................................. - 10 -
第7章 hello的存储管理...................... - 11 -
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 -
7.9动态存储分配管理.............................. - 11 -
7.10本章小结............................................ - 12 -
第8章 hello的IO管理....................... - 13 -
8.1 Linux的IO设备管理方法................. - 13 -
8.2 简述Unix IO接口及其函数.............. - 13 -
8.3 printf的实现分析.............................. - 13 -
8.4 getchar的实现分析.......................... - 13 -
8.5本章小结.............................................. - 13 -
结论............................................................... - 14 -
附件............................................................... - 15 -
参考文献....................................................... - 16 -
第1章 概述
1.1 Hello简介
P2P(From Program to Process)过程:
hello的生命周期是从一个高级C语言程序开始的,分为四个阶段:首先经过预处理器cpp进行预处理,生成文本文件hello.i,然后经过编译器ccl生成hello.s汇编程序,接着经过汇编器as生成hello.o文件,最后经过链接器ld将其与引用到的库函数链接,生成可执行文件hello。再通过系统创建一个新进程并且把程序内容加载,实现有程序到进程的转化。
O2O(From Zero-0 to Zero-0)过程:
程序运行前,shell调用execve函数将hello程序加载到相应的上下文中,将程序内容载入物理内存。然后调用main函数。程序运行结束后,父进程回收进程,释放虚拟内存空间,删除相关内容。这就是hello.c的O2O过程。
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第1张图片](http://img.e-com-net.com/image/info8/046ba35eb20a4be39b963049778d5b32.jpg)
《图1 hello的编译运行过程》
1.2 环境与工具
硬件环境:X64 CPU;2GHz;4G RAM;256GSSD Disk 以上
软件环境:Windows11 64位;VMware Workstation Player 17;Ubuntu 22.04.2
开发和调试工具:gdb;edb;readelf;objdump;Code::Blocks20.03
1.3中间结果
编写这篇文章生成的中间结果文件以及文件内容如下表格所示
hello.i:hello.c预处理后的文件。
hello.s:hello.i编译后的文件。
hello.o:hello.s汇编后的文件。
hello:hello.o链接后的文件。
hello1asm.txt:hello.o反汇编后代码。
hello2asm.txt:hello反汇编后代码。
hello.o_elf:hello.o用readelf -a hello.o指令生成的文件。
hello_elf:hello用readelf -a hello指令生成的文件。
1.4 本章小结
本章根据hello的自白,概括介绍了hello的P2P和O2O的过程。此外,还介绍了本实验用到的硬软件环境和开发调试工具。
第2章 预处理
2.1 预处理的概念与作用
2.1.1预处理的概念:
在编译源文件时,系统会自动使用预处理程序对源程序中的预处理部分进行处理,并在处理完毕后自动进入对源程序的编译阶段。C语言提供了多种预处理功能,主要用于处理以#开头的预处理指令,例如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。
2.1.2预处理的作用:
所有预处理器(cpp)命令都以井号(#)开头。井号必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。
以下是预处理器的功能和指令解释:
(1)在#include处添加对应的头文件。
(2)删除#define并展开相应的宏,使用#undef取消已经定义的宏。
(3)#ifdef用于判断宏是否已经定义,若已定义则返回真;#ifndef用于判断宏是否未定义,若未定义则返回真。
(4)处理所有条件预编译指令,例如#if和#endif。根据#if后面的条件决定需要编译的代码。
(5)#if用于判断给定条件是否为真,若为真则编译下面的代码;#else可用作#if的替代方案;#elif用于在前面的#if给定条件不为真时,判断当前条件是否为真,若为真则编译下面的代码;#endif用于结束#if……#else条件编译块。
(6)#error用于在遇到标准错误时输出错误信息。
(7)#pragma用于使用标准化方法,向编译器发布特殊的命令。
2.2在Ubuntu下预处理的命令
在终端中执行这个命令,gcc -E是使用GCC编译器的预处理功能,它将对hello.c文件进行预处理,并将结果输出到hello.i文件中。另一种方法是使用cpp命令,它也可以对hello.c文件进行预处理,并将结果通过重定向操作符(>)输出到hello.i文件中。无论使用哪种方法,都能生成名为hello.i的文本文件。
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第2张图片](http://img.e-com-net.com/image/info8/f976126f119e467ea05a7614cec2cb35.jpg)
《图2-2-1 Ubuntu下预处理指令gcc及结果》
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第3张图片](http://img.e-com-net.com/image/info8/fdbc5f88240e45e2b877d1ac662aac61.jpg)
《图2-2-2 Ubuntu下预处理指令cpp及结果》
2.3 Hello的预处理结果解析
在hello.i文件中,你可以看到经过预处理后的代码,其中已经去除了注释。文件的开头部分包含了所需的头文件,用于引入程序中所需的函数和定义。而文件的末尾部分(3077行至3091行)是与hello.c文件中的main函数完全一致的代码。这意味着在预处理阶段,hello.i文件中的代码已经展开并与原始代码中的main函数完全一致。
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第4张图片](http://img.e-com-net.com/image/info8/354008a3d03143e49870449a03edc004.jpg)
《图2-3-1 hello.c预处理结果》
2.4 本章小结
在本章中,我们首先解释了预处理的概念,即在编译之前对源代码进行处理的过程。预处理的作用是通过执行预处理指令,如宏定义、文件包含和条件编译等,对源代码进行修改和准备,以便后续的编译过程能够顺利进行。
接着,我们以hello.c程序为例,展示了如何在Ubuntu操作系统下进行程序的预处理。通过使用gcc编译器的预处理功能,我们执行了相应的命令,对hello.c文件进行了预处理操作。预处理的结果是生成了一个经过宏展开、文件包含等处理的新文件,即hello.i。
最后,我们对预处理结果进行了分析。通过观察hello.i文件,我们可以看到注释已经消失,程序加载了所需的头文件,并且最后的代码部分与原始代码中的main函数完全一致。这样的分析帮助我们理解预处理的过程和影响,以便更好地理解和优化程序。
第3章 编译
3.1 编译的概念与作用
3.1.1编译的概念:
在编译过程中,hello.i文件是经过预处理后的代码,其中包含了宏展开、文件包含和其他预处理指令的结果。接下来,编译器将对hello.i文件进行翻译,将其转换为另一个文本文件hello.s。
hello.s文件是汇编语言程序的表示形式,它包含了一系列汇编指令,用于表示原始代码的底层操作和指令。这个文件是编译器将高级语言代码转换为机器可执行指令的中间步骤。
通过将hello.i翻译为hello.s,编译器将代码从高级语言转换为汇编语言,为后续的汇编过程做准备。这个过程将源代码转化为更接近底层的指令级表示,为最终生成可执行文件做准备。
3.1.2编译的作用:
高级语言是一种人们容易理解和编写的编程语言,它使用类似自然语言的语法和结构。然而,计算机不能直接理解和执行高级语言代码。因此,需要通过编译过程将高级语言代码转换为机器指令,这些指令是计算机可以直接执行的低级指令。
机器指令是计算机硬件能够理解和执行的指令,它们以二进制形式表示,并对应于特定的操作和数据处理。与之对应的是汇编指令,它们以助记符的形式表示机器指令,使得人们更容易理解和编写这些指令。
通过将高级语言源程序转换为机器指令,计算机可以更轻松地理解和执行代码,而且这些指令与汇编指令一一对应。这样的转换过程为后续的汇编过程做好了准备,使得代码可以进一步转换为底层的机器语言,并最终生成可执行文件,使计算机能够正确执行程序。
3.2 在Ubuntu下编译的命令
打开终端,输入cc1 hello.i -o hello.s 或 gcc -S hello.c -o hello.s
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第5张图片](http://img.e-com-net.com/image/info8/ee539daa14af45ff83dfee1dc7415f56.jpg)
《图3-2-2 Ubuntu下编译gcc》
3.3 Hello的编译结果解析
编译过程是整个过程构建的核心部分,编译成功,源代码会从文本形式转换为机器语言。
下面是hello.s汇编文件内容:
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第6张图片](http://img.e-com-net.com/image/info8/3b7e35f90dd5406c865fd787cbd9b336.jpg)
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第7张图片](http://img.e-com-net.com/image/info8/081d71ba74794db6b22acf13aaf191e0.jpg)
《图3-3-1 hello.s文件内容》
3.3.1对文件信息的记录
![](http://img.e-com-net.com/image/info8/cffc53428a1f432b970bd1d861344e87.jpg)
《图3-3-1 hello.s文件内容》
该代码是汇编文件(.s)部分和文件信息的部分。
.file "hello.c": 此指示符表示当前汇编文件是由名为 "hello.c" 的源文件创建的。 源文件名称可以与调试信息相连接,供调试器使用。
.text: 此指示符定义了代码部分 。 代码部分是程序指令( 机械或汇编)所在的位置。 text部分将包含程序的主要代码 。
. section. rodata: 此指示符定义了只读数据部分。 rodata 部分用于存储程序中使用的字符串常数或其他只读数据 。
.align 8: 此指示符设置数据的排序 。 8表示在这里排列成8字节。 数据对齐是为了提高数据访问效率而在存储器上调整数据布局的工作。
.file "hello.c" 显示源文件的信息, .text 定义了代码部分。 section.rodata 设置了只读数据部分, .align 8 调整数据的排序。 这些切片和对齐设置用于控制组装代码的结构和数据访问的效率。
3.3.2对局部变量的操作
局部变量存储在栈中,当进入函数main的时候,会根据局部变量的需求,在栈上申请一段空间供局部变量使用。当局部变量的生命周期结束后,会在栈上释放。
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第8张图片](http://img.e-com-net.com/image/info8/7a9861a94b284136aac3b8bbf6f2ce38.jpg)
《图3-3-2-1 hello.s文件内容》
局部变量操作在main函数内进行。 下面我们来看一下代码块,以说明给定代码中的本地变量操作。:
上述代码在进入main函数时,设置堆栈框架,为局部变量分配空间。 现在我将详细说明代码块。
pushq%rbp:将当前函数的先前基线指针值保存到堆栈中。 这是维持先前堆栈框架的基点的工作。
.cfi_def_cfa_offset 16: 通过调整当前堆栈框架的大小来确保参数和本地变量的空间。
将 .cfi_offset 6,-16:% rbp 寄存器值存储在当前堆栈框架中的 -16 位。 这是一项提供调试信息的工作。
movq%rsp,%rbp: 将当前堆栈指针值设置为基线指针,生成新的堆栈框架。
.cfi_def_cfa_register 6:将当前堆栈指针寄存器(%rsp)设置为基指针寄存器(%rbp)。
32 美元,% rsp : 从堆栈框架中分配 32 字节用于本地变量的空间。
movl%edi,-20(%rbp):将第一个参数保存在堆栈框架内的-20(%rbp)位置。 -20(%rbp) 是第一个本地变量的内存位置。
movq%rsi,-32(%rbp):将第二个参数保存在堆栈框架内的-32(%rbp)位置。 -32(%rbp)是内存位置,指向第二个本地变量。
对于局部变量操作,设置堆栈框架,参数存储在局部变量中。 -20(%rbp)位置存储第一个参数,-32(%rbp)位置存储第二个参数。 此后,代码将使用相应的本地变量执行程序操作。
3.3.3对字符串常量的操作
![](http://img.e-com-net.com/image/info8/dbe3af738d2d4eb3b951d1ce95ae0bea.jpg)
《图3-3-3 hello.s文件内容》
上述代码定义了.LC0和.LC1标签识别的字符串常数。 现在我将详细说明代码块。
.LC0:: .定义标签标识为 LC0 的字符串常数 。LC0是程序中使用的第一个字符串。
.string "\347\224\250\346\263\225:Hello 2021130013 JM\347\247\222\346\225\260\357\274\201":这部分表示实际字符串常数。 字符串用双引号"(") 绑定,以C弦形式表示。 该字符串表示为Unicode代码点的序列,使用背斜和数字组合 (\347\224\250) 表示Unicode字符。 此字符串表示" Hello 2021130013 JM 学生" 。
.LC1:: .定义标签标识为 LC1 的字符串常数 。LC1是程序中使用的第二个字符串。
.string "Hello%s%s\n" : 此部分表示第二个字符串常数 。 此字符串表示 "Hello%s%s\n" 。 在这里,%s 是字符串格式的指定者, 用于在随后的代码中替换字符串值 。
上述代码块用于定义字符串常数,并在随后的程序中参照它们进行输出或格式化字符串生成等操作。
3.3.4参数传递------对main的参数argv的传递![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第9张图片](http://img.e-com-net.com/image/info8/43c473428880429c90f0c56ee81bad3a.jpg)
《图3-3-4 hello.s文件内容》
该代码负责将main函数的参数传递给argv。 我现在详细说明一下代码。
movq-32(%rbp),%rax:将第一个参数argv的地址加载到%rax寄存器中。 -32(%rbp)表示第二个位于堆栈框架内的参数的地址。
通过将 addq $ 16,%rax:%rax 加上 16 来计算 argv[1] 的地址。 argv是参数排列的指针,因此argv+16指向第二个参数argv[1]的地址。
将movq(%rax),%rdx:argv[1]的值加载到%rdx寄存器中。 %rdx是第二个参数argv[1]的值将被保存的寄存器。
movq-32(%rbp),%rax: 再次将argv的地址加载到%rax寄存器中。
通过将8加到addq$8,%rax:%rax来计算argv[0]的地址。 argv是参数排列的指针,因此argv+8是第一个参数argv[0]的地址。
将 movq (%rax) 和%rax: argv[0] 的值加载到%rax 寄存器中。 %rax是第一个参数argv[0]的值将被保存的寄存器。
将存储在movq%rax,%rsi:%rax中的argv[0]值复制到%rsi寄存器中。 它将被用作main函数内字符串格式的参数。
上述代码块负责通过访问main函数中的argv参数来加载相应值。 在随后的程序中,可以通过使用储存在%rdx和%rsi寄存器中的值来访问argv中的元素或转移到其他函数。
3.3.5 数组的操作
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第10张图片](http://img.e-com-net.com/image/info8/5ba8ce2fb4cc4883b8558cd6b0daf22d.jpg)
《图3-3-5 hello.s文件内容》
该代码在main函数中起到运行阵列的作用。 我现在详细说明一下代码。
以.L4:: 标签. L4 开头的块 。 这个块代表重复句。
movq-32(%rbp),%rax:argv的地址加载到%rax寄存器中。
addq$24,%rax:%rax加上24,计算argv[3]的地址。 argv是参数排列的指针,因此argv+24指向第四个参数argv[3]的地址。
将 movq (%rax)、%rax:argv[3] 的值加载到%rax 寄存器中。 存储在 argv[3] 中的值被存储在 %rax 中。
movq%rax,%rdi:%rax中保存的值复制到%rdi寄存器中。 它将作为atoi函数的因子。
调用 call atoi @PLT: atoi 函数将% rdi 中的参数转换为整数。
将movl%eax,%edi:atoi函数的返回值从%eax寄存器复制到%edi寄存器。 它将作为sleep函数的参数。
调用 call sleep@PLT: sleep 函数, 按 %edi 规定的时间暂停程序 。
addl$1,-4(%rbp):重复变量-4(%rbp)加上1。 这个变量用于重复句子的控制。
以.L3:: 标签. L3 开头的块 。 该块表示重复句的条件检查。
将cmpl$7,-4(%rbp):-4(%rbp)中存储的值与7进行比较。 这是确认重复句的终止条件。
jle.L4:如果条件真实(-4(%rbp)小于7),跳到L4区块,重新运行重复句。
上述代码是将argv[3]的值转换为整数后,呼叫sleep函数,执行按规定时间暂停程序的排列动作代码。 通过重复句增加排列的索引并重复动作。
3.3.6对函数的调用与返回
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第11张图片](http://img.e-com-net.com/image/info8/0631d047adae4eebb59176057aff7ae2.jpg)
《图3-3-6 hello.s文件内容》
该代码表示从main函数调用其他函数并接受结果的部分。 我现在详细说明一下代码。
movq-32(%rbp),%rax:argv的地址加载到%rax寄存器中。
通过将 addq $ 16,%rax:%rax 加上 16 来计算 argv[1] 的地址。 argv是参数排列的指针,因此argv+16指向第二个参数argv[1]的地址。
将movq(%rax),%rdx:argv[1]的值加载到%rdx寄存器中。 存储在argv[1]中的值被存储在%rdx中。
movq-32(%rbp),%rax:argv的地址重新加载到%rax寄存器中。
通过将8加到addq$8,%rax:%rax来计算argv[0]的地址。 argv是参数排列的指针,因此argv+8是第一个参数argv[0]的地址。
将 movq (%rax) 和%rax: argv[0] 的值加载到%rax 寄存器中。 存储在 argv[0] 中的值被存储在 %rax 中。
movq%rax,%rsi:%rax中保存的值复制到%rsi寄存器中。 这将作为printf函数的第二个因子。
将leaq.LC1(%rip)、%rax:.LC1字符串的地址加载到%rax寄存器中。 %rip是当前指令地址的特殊寄存器,因此.LC1的地址是通过%rip计算的。
用%rdi寄存器复制movq%rax,%rdi:%rax中存储的地址。 这将作为printf函数的第一个因子。
在 movl $0,% eax:% eax 寄存器中保存 0 。 它用作存储printf函数的返回值的空间。
调用 call printf@PLT: printf 函数 。 将存储在%rdi和%rsi中的参数传递,函数调用后返回值存储在%eax中。
movq-32(%rbp),%rax:argv的地址重新加载到%rax寄存器中。
addq$24,通过将24加到%rax:%rax来计算argv[2] argv是参数排列的指针,因此argv+24是第三个参数argv[2]的地址。
将 movq (%rax)、%rax:argv[2] 的值加载到%rax 寄存器中。 存储在argv[2]中的值被存储在%rax中。
movq%rax,%rdi:%rax中保存的值复制到%rdi寄存器中。 它将作为atoi函数的因子。
调用 call atoi @PLT: atoi 函数 。 将存储在%rdi中的参数传递,函数调用后返回值将存储在%eax中。
将 movl%eax,%edi:%eax 保存的值复制到%edi 寄存器中。 它将作为sleep函数的参数。
调用 call sleep@PLT: sleep 函数。 传递存储在% edi 中的参数 。
上述代码表示通过调用printf、atoi、sleep等函数来执行任务的部分,并显示了每个函数的因子传递方法和调用方法。
3.3.7 for循环
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第12张图片](http://img.e-com-net.com/image/info8/a01af7942cb74cdb90f4b9729725553b.jpg)
《图3-3-6 hello.s文件内容》
代码表示 for 重复句子的内容, 重复以下过程 :
1. 调用 printf 函数输出字符串 。
2. 调用atoi函数将字符串转换为整数。
3. 调用 sleep 函数在指定时间内暂停应用程序。
3、重复句计数增加1。
4.检查重复句的终止条件,符合条件的,再重复一遍。
构成 for 环的部分是 printf、atoi 和 sleep 函数,在调用函数的同时传递参数,并执行七次重复。
3.3.8 赋值操作
hello.s代码使用堆栈框架和寄存器来管理全局变量和本地变量的内存位置。 对于全局变量,初始值或 0 的初始化存储空间被分配给 .data 或 .bss 部分,当访问该变量时,将使用存储地址。
对于局部变量,在堆栈框架内分配,在main函数内使用的局部变量作为堆栈框架的偏移接近。 例如,movl%edi,-20(%rbp)负责将main函数的第一个因数存储在堆栈框架中的-20(%rbp)位置。
因此,在hello.s代码中,通过操纵存储位置在变量中存储或加载值,表明与变量分配相关的低级别操作。
3.4 本章小结
这一章首先介绍了编译的概念和作用,并以hello.s为例在Ubuntu下进行了分析,通过分析汇编程序来理解编译器如何处理不同的数据类型和各种操作。编译是将高级语言程序转化为可执行文件的关键步骤之一。编译器负责将源代码翻译成机器可以理解的指令,包括处理数据类型、变量分配、函数调用等任务。通过分析汇编程序,我们可以更深入地了解编译器的工作原理,以及如何优化生成的机器代码。这对于开发者来说非常重要,因为它可以帮助我们更好地理解程序的执行过程,优化代码性能,并解决可能出现的问题。
第4章 汇编
4.1 汇编的概念与作用
4.1.1汇编的概念:
汇编器(as)将汇编程序翻译成机器语言指令,然后将这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件中。目标文件是一个二进制文件,无法直接以文本形式打开,因为它包含了机器语言指令,而非可读的文本字符。目标文件包含了程序的机器代码表示,其中每条指令都对应着特定的操作。这些指令被打包成一种特定的格式,以便在后续的链接过程中能够正确地连接到其他目标文件和库文件中。目标文件是编译过程的中间产物,它为链接器提供了必要的信息,以便将多个目标文件组合成最终的可执行文件。通过汇编器的工作,我们能够将高级语言编写的程序转换为机器能够执行的指令,为程序的最终运行做准备。
4.1.2汇编的作用:
生成机器指令,方便机器直接分析。
4.2 在Ubuntu下汇编的命令
打开终端输入 as hello.s -o hello.o 或 gcc -c hello.s -o hello.o
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第13张图片](http://img.e-com-net.com/image/info8/9c2a8ed8385342e5a0ecca2746cbcdf2.jpg)
《图4-2-2汇编生成hello.o文件》
4.3 可重定位目标elf格式
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第14张图片](http://img.e-com-net.com/image/info8/82f41a9c4292496e838a4849f06e8f15.jpg)
《图4-3-1 典型的ELF可重定位目标文件》
1. ELF头以16字节的序列开始,描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助两届其语法分析和解释目标文件的信息,包括ELF头的大小、目标文件的类型(如可执行、可重定位或者共享的)、机器类型、节头部表的文件偏移以及节头部表中条目的大小和数量。
2. .text:已编译程序的机器代码。
3. .rodata:只读数据。
4. .data:已初始化的全局变量和局部静态变量。
5. .bss:未初始化的全局变量和局部静态变量,仅是占位符,不占据任何实际磁盘空间。
6. .symtab:符号表,存放函数和全局变量(符号表)信息,不包括局部变量。
7. .rel.text:.text节的重定位信息,用于重新修改代码段的指令中的地址信息。
8. .rel.data:.data节的重定位信息,用于对被模块使用或定义的全局变量重定位的信息。
9. .debug:调试符号表,只有以-g方式调用编译器驱动程序时,才会得到这张表。
10. .line:原始C源程序中的行号和.text节中机器指令之间的映射。
11. .strtab节:字符串表,包括.symtab和.debug节中的符号表。
12. 节头表:每个节的节名、偏移和大小。
4.3.2 hello.o分析
1.首先,打开终端,用readelf -S指令查看hello.o的节头表,查看节的基本信息。除了ELF头外,节头表的是ELF可重定位目标文件中最重要的部分。描述了每个节的节名文件偏移、大小、访问属性、对齐方式等。
下图中,Name列为节名,Type为类型,Offset为起始地址,Size为大小。
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第15张图片](http://img.e-com-net.com/image/info8/c1618e57ec044119946cca197f8c8ff6.jpg)
《 图4-3-2-1 readelf -S指令查看节头表》
2. 用readelf -h指令可以查看hello.o的ELF头信息。
Class:64位版本
Data:使用补码表示,且为小端法
Version:版本为1
OS/ABI:操作系统为UNIX – SYSTEM V
TYPE:REL表明这是一个可重定位文件
Machine:64位机器上编译的目标代码为Advanced Micro Devices X86-64
Entry point address:为0x1100表示程序的入口地址为0x1100
Start of program headers:为64表示没有程序头表
Start of section headers:节头表的起始位置为14184字节处
Size of section headers:64表示每个表项64个字节
Number of section headers:31表示共31个表
Section header string table index:30为.strtab在节头表中的索引
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第17张图片](http://img.e-com-net.com/image/info8/ab403a8bfcd84993a837accba6080040.jpg)
《图4-3-2-2 readelf -h指令查看ELF头》
3. readelf -s hello.o查看符号表,Name为符号名称,Value是符号相对于目标节的起始位置偏移,Size为目标大小,Type是类型,数据或函数,Bind表示是本地还是全局。
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第18张图片](http://img.e-com-net.com/image/info8/295cc90926764b85b83d6753c2a59e08.jpg)
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第19张图片](http://img.e-com-net.com/image/info8/2b8a0e9821684621b8e65060d9a9b30b.jpg)
《 图4-3-2-3 readelf -s指令查看符号表》
4, readelf-rhello.o发现hello.o文件中没有.rel.data片段,即.data片段不需要额外的重新定位信息。 以上代码的特征及说明如下。
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第20张图片](http://img.e-com-net.com/image/info8/788a4e7916054aa7bf99c2fecf17829e.jpg)
《图4-3-2-4 readelf -r指令查看重定位信息》
说明:
. rela. Dyn 重播部分 :
第一项:关于Offset0x3d90位置的取景信息。 R_X86_64_RELATIVE类型,Sym。 值是0x11e0。
第二项:关于Offset0x3d98位置的取景信息。 R_X86_64_RELATIVE类型,Sym。 值是0x11a0。
第3项:关于Offset0x4008位置的取景信息。 R_X86_64_RELATIVE类型,Sym。 维是0x4008。
其余项为R_X86_64_GLOB_DAT类型,分别表示与特定符号连接的值。
. rela.plt 重播部分 :
第1项到第6项各特定函数的取景信息。 R_X86_64_JUMP_SLOT类型,分别将函数的地址设置为连接符号的值。
回放部分包括对象文件链接时用于符号解析和重新配置操作的信息。 基于这些信息,接头会在运行时间解决符号,并将对象文件中的代码和数据重新定位到正确的地址。
该代码的特征如下。
1. 重播部分信息:通过readelf-rhello.o指令输出的信息显示对象文件重播部分的内容。 回放部分提供了在链接过程中用于符号解决和重新部署的信息。
2. 回放类型:从打印的信息中可以识别回放项目的类型。 主要可以看到R_X86_64_RELATIVE和R_X86_64_GLOB_DAT类型。 每种类型代表重新部署操作的特定方式,并指定了处理与符号相关联的值的方法。
3.符号和符号值:在回放部分信息中,还显示各项目符号和相应符号的值(地址)。 这使得链接器能够在运行时解决符号并正确调整对象文件的地址。
4. 链接库函数:在回放部分信息中,还可以包含链接库函数的项目。 这代表了程序中使用的外部函数的符号解决和重新配置。
5. readelf -g hello.o显示节组信息。hello.o没有节组
![](http://img.e-com-net.com/image/info8/f882e9f08099483690dd8d5dec80be28.jpg)
《图4-3-2-5 readelf -S指令查看节组信息》
4.4 Hello.o的结果解析
在终端输入objdump -d -r hello.o查看hello.o的反汇编,结果如下:
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第21张图片](http://img.e-com-net.com/image/info8/d9d416427cd940febeb5c4f0b0f2a1ca.jpg)
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第22张图片](http://img.e-com-net.com/image/info8/36ab642e2e0f4b43bade62d901c31932.jpg)
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第23张图片](http://img.e-com-net.com/image/info8/bb45085de7d148ef95b9c9879c0c6683.jpg)
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第24张图片](http://img.e-com-net.com/image/info8/f2fa4882490d4b5ebd209968906e2238.jpg)
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第25张图片](http://img.e-com-net.com/image/info8/670be341e90046f6a6f1818d3cc10481.jpg)
《图4-4-1 objdump指令查看反汇编》
4.5 本章小结
本章首先介绍了汇编的概念和作用,汇编是将高级语言程序翻译成机器语言指令的过程。接着,我们以hello.s文件为例进行了实际操作,将其汇编成了ELF可重定位目标文件hello.o。然后,我们使用readelf工具并设置不同的参数,查看了hello.o的ELF头、节头表、可重定位信息和符号表等内容。通过这些分析,我们深入理解了可重定位目标文件的组成和结构。最后,我们将hello.o与hello.s进行比较,分析它们之间的差异,并说明机器语言和汇编语言之间的一一对应关系。
第5章 链接
5.1 链接的概念与作用
5.1.1链接的概念
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存并执行。
5.1.2链接的作用
链接的作用是将分离编译的模块组合在一起,形成一个完整的应用程序。通过链接,一个大型的应用程序可以被分解为更小、更易于管理的模块。每个模块可以独立地进行修改和编译,而无需重新编译整个应用程序。这种模块化的设计使得开发者能够更加灵活地对应用程序进行维护和更新,提高了开发的效率和可维护性。同时,链接还负责解析模块之间的依赖关系,确保各个模块之间的函数和数据能够正确地相互调用和共享。因此,链接在软件开发过程中起着至关重要的作用,它使得应用程序的开发、管理和维护变得更加便捷和高效。
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
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第26张图片](http://img.e-com-net.com/image/info8/e88f167ae85d4c30917a9994c650aacd.jpg)
《图5-2-1 Ubuntu下链接ld指令》
5.3 可执行目标文件hello的格式
可执行目标文件和可重新配置文件存在一些差异。 e_entry 是 ELF 报头的字段,它在程序运行时提供了第一个命令的地址,在可重新配置的文件中,该字段为 0。 可执行的目的文件增加了结构排列,也称为程序头表或分段头表。 此外,还添加了.init部分,用于定义init函数。 当可执行目标文件开始运行时,该函数会执行初始化操作。 与可重新部署的目的文件相比,两个.rel部分更少,因为可执行的目的文件不需要重新部署。
查看hello的ELF头:发现hello的ELF头中Type处显示的是EXEC,表示时可执行目标文件,这与hello.o不同。hello中的节的数量为27个
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第27张图片](http://img.e-com-net.com/image/info8/1336575413314b3c97285fb23e6b25b6.jpg)
《图5-3-2 readelf -h查看hello的ELF头》
查看hello的节头表
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第28张图片](http://img.e-com-net.com/image/info8/5a59cf6c8d49401aa2f12c5a1be1250f.jpg)
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第29张图片](http://img.e-com-net.com/image/info8/42ad77b586ac4893bfe0fb236f69a590.jpg)
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第30张图片](http://img.e-com-net.com/image/info8/420bb33d31c74a22aecaf20b243a3142.jpg)
《 图5-3-3 readelf -S查看hello的节头表》
发现刚才提到的27个节的具体信息,在节头表中都有显示,包括大小Size,偏移量Offset,其中Address是程序被载入虚址地址的起始地址。
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第31张图片](http://img.e-com-net.com/image/info8/1ba10b750d6a4628891c6ba09a7e77a9.jpg)
《图5-3-7 readelf -l查看hello的程序头表》
通过查看上述代码结果,可以获得以下信息。:
Elf 文件类型: EXEC (可执行文件)
进入点( Entry point) : 0x4010f0
程序报头数: 12 个
程序报头(Program Headers)信息中需要关注的部分如下。:
1. PHDR:本部分代表程序报头表。 类型为PHDR,偏移量为0x40。 此节的大小和内存大小为 0x2a0, 具有只读( R) 属性。
2. INTERP:本部分包含有关程序插件的信息。 类型为INTERP,包含请求的程序接口(/lib64/ld-linux-x86-64.so .2)的信息。 偏移量为0x2e0,大小和内存大小分别为0x1c。
3. LOAD: 这些部分代表应用程序的加载信息。 LOAD类型的部分是装入存储器的部分。
第一个LOAD部分从地址0x400000开始,大小和内存大小分别为0x5f0。 具有只读( R) 属性 。
第二个LOAD部分从地址0x401000开始,大小和内存大小分别为0x1cd。 具有可读性和可执行( E) 属性。
第三个LOAD部分从地址0x402000开始,大小和内存大小分别为0xe0。 具有只读( R) 属性 。
第四个LOAD部分从地址0x403e50开始,大小和内存大小分别为0x1fc。 具有可读可写( RW) 属性。
4. DYNAMIC:本节包含动态链接信息。 偏移量为0x2e50,大小和内存大小分别为0x1a0。 具有可读可写( RW) 属性。
5. NOTE: 这些部分代表笔记本信息。 主要内容如下:
第一个NOTE部分从地址0x400300开始,大小和内存大小分别为0x30。 具有只读( R) 属性 。
第二个NOTE部分从地址0x400330开始,大小和内存大小分别为0x20。 具有只读( R) 属性 。
6. GNU_STACK:本部分表示有关堆栈操作的信息。 具有可读可写(RW)属性,地址和大小为0。
7. GNU_RELRO:本部分指只读部分和只读(RELRO)部分用于动态重排的信息。 具有只读(R)属性,偏移量为0x2e50,大小和内存大小分别为0x1b0。
最后,通过片段和片段之间的映射信息,可以确定每个片段属于哪个片段。 上述信息中明确规定了部分属于哪个部分。 如.interp段属于第01段,.note.gnu.property段和.note.ABI-tag段属于第02段。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
由下图,可以看到虚拟地址空间的起始地址为0x400000。
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第32张图片](http://img.e-com-net.com/image/info8/001b86d8bab44e2b801c742a1e4ddf7e.jpg)
《图5-4-1 edb查看hello的虚拟地址空间》
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第33张图片](http://img.e-com-net.com/image/info8/d0ca6a752a6d4187904f976690771814.jpg)
《由图5-3-7可以知道,.inerp段的起始地址为04002e0 》
可以在edb中可以找到:
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第34张图片](http://img.e-com-net.com/image/info8/9cdba16cef2f40e5adca9916f416df7e.jpg)
《图5-4-3 edb查看.inerp段信息》
.text段的起始地址为0x4010f0,.rodata段的起始地址为0x402000,也都可以在edb中找到
![](http://img.e-com-net.com/image/info8/5c0fb8d7f42a4d32b754fb9fd6b93f9b.jpg)
《图5-4-4 readelf查看.text段信息》
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第35张图片](http://img.e-com-net.com/image/info8/426c13c079f0481e9c6002072e1dcd80.jpg)
《图5-4-5 edb查看.text段信息 》
![](http://img.e-com-net.com/image/info8/b450180746694e319f3dd0bb86a06b24.jpg)
《图5-4-6 readelf查看.rodata段信息》
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第36张图片](http://img.e-com-net.com/image/info8/7fb278bd596d476a8ed1dbea644f5a0d.jpg)
《图5-4-7 edb查看.rodata段信息》
5.5 链接的重定位过程分析
首先,使用objdump -d -r hello对hello进行反汇编,结果如下:
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第37张图片](http://img.e-com-net.com/image/info8/764488f4b67843d2ba85eb5c6a4880c1.jpg)
《图5-5-1 objdump查看hello反汇编结果》
hello的反汇编代码多了很多节,并且发现每条数据和指令都已经确定好了虚拟地址,不再是hello.o中的偏移量。通过链接之后,也含有了库函数的代码标识信息。
接着,我们具体比较分析一下hello和hello.o的反汇编结果,下面两个图分别为hello.o和hello的反汇编的部分截图,其余同理。
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第38张图片](http://img.e-com-net.com/image/info8/ec98ff0b92a946c4ad81634b280f942f.jpg)
《图5-5-2 hello反汇编部分截图》
![](http://img.e-com-net.com/image/info8/7d8d101890b44fa285a04caf5c7f420d.jpg)
《 图5-5-3 hello反汇编部分截图》
图5-5-2为hello.o的反汇编代码,图5-5-3为hello的反汇编代码。可以看出,在hello.o中跳转指令和call指令后为绝对地址,而在hello中已经是重定位之后的虚拟地址。
接下来,以0x4011f6出的call指令为例,说明链接过程:
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第39张图片](http://img.e-com-net.com/image/info8/9c9ad57e4bcd42b993c1a21c1bb740b4.jpg)
《图5-5-4 readelf查看hello.o的重定位信息》
查看该图可知,此处应该绑定第0xc个符号,同时链接器知道这里是相对寻址。接着查看hello.o的符号表,找到第12个符号puts,此处绑定puts的地址。
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第40张图片](http://img.e-com-net.com/image/info8/58751f1bb52b49d9b3fa3e946c6b7903.jpg)
《图5-5-5 readelf查看hello.o的符号表》
在hello中找到puts的地址为0x401090。
![](http://img.e-com-net.com/image/info8/43600a7bde1e4fc48c6c4e15b70bb861.jpg)
《图5-5-6 readelf查看hello.o的符号表》
当前PC的值为call指令的下一条指令的地址,也就是0x4011fb。而我们要跳转到的地方为0x401090,差0x16b,因此PC需要减去0x16b,也就是加上0xff ff fe 95,由于是小端法,因此重定位目标处应该填入 95 fe ff ff。
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第41张图片](http://img.e-com-net.com/image/info8/112f1f6a88b24bbd83eecf8d2c396d4e.jpg)
《图5-5-7 hello.o反汇编》
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
子程序名 |
子程序地址 |
hello!_start |
0x00000000004010f0 |
hello!__libc_csu_init |
0x0000000000401270 |
hello!_init |
0x0000000000401000 |
hello!frame_dummy |
0x00000000004011d0 |
hello!register_tm_clones |
0x0000000000401160 |
hello!main |
0x00000000004011d6 |
hello!printf@plt |
0x0000000000401040 |
hello!atoi@plt |
0x0000000000401060 |
hello!sleep@plt |
0x0000000000401080 |
hello!getchar@plt |
0x0000000000401050 |
hello!exit@plt |
0x0000000000401070 |
hello!__do_global_dtors_aux |
0x00000000004011a0 |
hello!deregister_tm_clones |
0x0000000000401130 |
hello!_fini |
0x00000000004012e8 |
5.7 Hello的动态链接分析
动态的链接器在正常工作时链接器采取了延迟绑定的链接器策略,将过程地址的绑定推迟到第一次调用该过程时。
首先找到.got的地址为0x403ff0。
![](http://img.e-com-net.com/image/info8/e2ca0677a49a4831ade8c21a0701d877.jpg)
《图5-7-1 查看.got的地址》
在edb中找到相应地址,分析在dl_init前后,这些项的变化。
![](http://img.e-com-net.com/image/info8/f73239100ced4d988f8fdb23ceaa5ef4.jpg)
![](http://img.e-com-net.com/image/info8/5281cd2f58d241d58663f12e978d88bd.jpg)
《图5-7-2 dl_init前后》
5.8 本章小结
本章首先介绍了链接的概念和作用,详细说明了可执行目标文件的结构,及重定位过程。并且以可执行目标文件hello为例,具体分析了各个段、重定位过程、虚拟地址空间、执行流程等。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念:
进程是指在计算机中正在运行的程序。它是操作系统进行资源分配和调度的基本单位,也是操作系统结构的基石。
-
"计算机中正在运行的程序":指的是在计算机系统中活动的程序,也就是已经加载到内存中并正在执行的代码。
-
"资源分配和调度的基本单位":操作系统将系统资源(如CPU、内存和设备)分配给进程,并对它们进行调度,以便合理地利用计算机资源。
-
"操作系统结构的基石":进程是操作系统的基本概念和组织方式之一,它提供了对程序执行的控制和管理,为其他系统组件和功能提供支持。
换句话说,进程是计算机中正在运行的程序的抽象表示,它充当了操作系统与程序之间的桥梁,使得程序能够协调地使用计算机资源,并且在操作系统的管理下实现任务的执行和完成。
6.1.2 进程的作用:
提供给应用程序的关键抽象包括以下两个方面:
(1)独立的逻辑控制流:这一抽象为应用程序提供了一个假象,使得程序好像是独占地使用处理器进行运行。也就是说,每个应用程序都被赋予了自己独立的执行流,使得它可以按照自己的逻辑顺序执行指令,不受其他程序的干扰。这种抽象使得应用程序可以以串行的方式编写,无需关注其他同时运行的程序,从而简化了程序的设计和开发过程。
(2)私有的地址空间:这一抽象为应用程序提供了一个假象,使得程序好像是独占地使用内存系统。也就是说,每个应用程序都被分配了自己独立的地址空间,用于存储程序的指令、数据和运行时状态。这种抽象使得应用程序可以将内存视为私有的资源,无需担心与其他程序的内存冲突或数据共享问题。每个应用程序的地址空间相互隔离,确保了程序的安全性和稳定性。
综上所述,这两个关键抽象为应用程序提供了一种假象,使得它们好像独占地使用处理器和内存系统。这种抽象使得应用程序能够独立地执行和管理自己的逻辑控制流,并在私有的地址空间中存储和操作数据,从而实现了程序的独立性和隔离性。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1Shell-bash的作用:
Shell 是一个命令解释器,它负责解释用户输入的命令,并将这些命令传递给内核进行处理。Shell 还具有自己的编程语言,用于编写对命令进行操作的程序。通过这种编程语言,用户可以编写由一系列 Shell 命令组成的程序。Shell 编程语言具备许多常见编程语言的特点,例如循环结构和分支控制结构等,使用这种编程语言编写的 Shell 程序能够实现与其他应用程序相同的效果。
简而言之,Shell 是一种解释用户命令的程序,它具有自己的编程语言,可以用于编写一系列的命令操作。通过编写 Shell 程序,用户可以实现各种任务和操作,包括循环、条件判断等,使得 Shell 程序具备了与其他应用程序相似的功能和效果。
6.2.2 Shell-bash的处理流程:
Shell 首先从命令行中找出特殊字符(元字符),然后将这些元字符翻译成分隔符。接下来,命令行被划分为小块的 tokens,这些程序块 tokens 被处理,并检查它们是否是 Shell 中引用的关键字。一旦 tokens 被确定,Shell 根据别名文件中的列表来检查命令的第一个单词。如果这个单词在别名表中出现,就进行替换操作,并重新回到第一步重新分割程序块 tokens。
然后,Shell 执行对波浪符(~)以及所有带有美元符号($)的变量的替换,并将命令行中的内嵌命令表达式替换为实际的命令。随后,Shell 进行命令的检查,初始化所有输入输出重定向,并最终执行命令。
简而言之,Shell 首先对命令行进行解析,找出特殊字符并将其翻译为分隔符。然后,将命令行划分为小块的 tokens,并检查它们是否是 Shell 的关键字。接下来,Shell 根据别名表对命令行进行替换操作。然后,Shell 替换波浪符和变量,并将内嵌命令表达式替换为实际的命令。最后,Shell 检查命令并执行相应的操作,包括初始化输入输出重定向。
6.3 Hello的fork进程创建过程
进程的创建是通过fork函数实现的:pid_t fork(void)。创建的子进程会获得与父进程用户级虚拟地址空间相同但独立的一份副本,包括代码段、数据段、堆、共享库和用户栈。此外,子进程还会获得与父进程打开的任何文件描述符相同的副本。这意味着当父进程调用fork时,子进程可以读取父进程中已打开的任何文件。
简而言之,通过fork函数创建的子进程会获得与父进程相同但独立的用户虚拟地址空间副本,包括代码和数据段、堆、共享库以及用户栈。子进程还会继承父进程打开的文件描述符,这意味着子进程可以访问父进程中已打开的文件。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。它会加载并执行可执行目标文件hello,并传递参数列表argv和环境变量envp。只有在发生错误的情况下,例如找不到指定的文件名filename时,execve才会返回到调用程序。一旦execve成功执行,它将不会返回。与fork函数不同,fork函数调用一次会返回两次,而execve函数调用一次后永远不会返回。
简而言之,execve函数用于在当前进程上下文中加载和运行一个新程序。它加载并执行名为hello的可执行文件,并传递参数列表argv和环境变量envp。只有在出现错误时,比如无法找到指定的文件名filename,execve函数才会返回到调用程序。而一旦execve函数成功执行,它将不会返回,执行的控制权完全交给了新加载的程序。与fork函数不同,fork函数会返回两次,而execve函数只会调用一次且不会返回。
6.5 Hello的进程执行
1. 上下文信息:操作系统使用一种称为上下文切换的高级异常控制流来实现多任务。上下文是指进程自身的虚拟地址空间,包括用户级上下文和系统级上下文。每个进程的虚拟地址空间与进程本身一一对应(因此与进程ID一一对应)。由于每个CPU一次只能处理一个进程,而系统中通常有多个进程需要运行,因此处理器需要定期切换到新的进程来执行。这个切换的过程使不同进程之间的指令能够交替执行,被称为进程的上下文切换。
2. 进程时间片:
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第42张图片](http://img.e-com-net.com/image/info8/b03e0dfcb32b47f7a2b062e3ebceb32e.png)
如上图所示,为进程A与进程B之间的相互切换。处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,为用户模式;设置模式为为内核模式。用户模式就是运行相应进程的代码段的内容,此时进程不允许运行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;而内核模式中,进程可以运行任何指令。
6.6 hello的异常与信号处理
1. 异常和信号异常分为四类:中断、陷阱、故障、终止
2. hello程序可能遇到的异常情况:
(1)中断:异步发生。在执行hello程序时,由处理器外部的I/O设备信号触发。I/O设备通过处理器芯片上的引脚发送信号,并在系统总线上放置异常号以触发中断。该异常号标识引起中断的设备。在当前指令执行完毕后,处理器会注意到中断引脚电压的变高,然后从系统总线读取异常号,并调用相应的中断处理程序。在处理程序返回之前,将控制返回到下一条指令。结果就好像没有发生中断一样。
(2)陷阱:有意引发的异常,是执行特定指令的结果。在执行hello程序的sleep函数时可能会出现此异常。
(3)故障:由错误引起,可能由故障处理程序修复。在执行hello程序时,可能会出现页面错误。
(4)终止:由不可恢复的致命错误引起,通常是一些硬件错误,例如DRAM或SRAM位损坏时发生的奇偶错误。
3. 键盘上操作导致的异常:
(1)运行时输入回车:
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第43张图片](http://img.e-com-net.com/image/info8/8f8f16d70a08400e82e58498d6f86f45.jpg)
《图6-6-1 运行hello时输入回车》
(2)运行时输入Ctrl+C
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第44张图片](http://img.e-com-net.com/image/info8/63e6fa3f50f84a92a99c941ee9d7d6de.jpg)
《图6-6-2 运行hello时输入Ctrl+C》
(3)运行时输入Ctrl+Z
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第45张图片](http://img.e-com-net.com/image/info8/5ab6fb432a704989b44f6d1b4d5544ac.jpg)
《图6-6-3 运行hello时输入Ctrl+Z》
输入ps,监视后台程序
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第46张图片](http://img.e-com-net.com/image/info8/621e58059e164edcafcbf0ce4f35c67a.jpg)
《图6-6-4 输入ps》
输入jobs,显示当前暂停的进程
![](http://img.e-com-net.com/image/info8/d41ce67a14654232a169e733238af166.jpg)
《图6-6-5 输入jobs》
输入pstree,以树状图形式显示所有进程
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第47张图片](http://img.e-com-net.com/image/info8/e07ccaed036d4c35be80798de8337aa9.jpg)
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第48张图片](http://img.e-com-net.com/image/info8/26e0d2e94a284a6ea6725cb8034c8654.jpg)
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第49张图片](http://img.e-com-net.com/image/info8/98bd128364f44349a5cc96c85bc6fdcf.jpg)
《图6-6-6 输入pstree》
输入fg,使停止的进程收到SIGCONT信号,重新在前台运行。
![](http://img.e-com-net.com/image/info8/42c46ce581e2427fa4580a34d74ea0a7.jpg)
《图6-6-7 输入fg》
输入kill,-9表示给进程3226发送9号信号,即SIGKILL,杀死进程。
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第50张图片](http://img.e-com-net.com/image/info8/27d4683e3b59447fb7f725ad37b452f0.jpg)
《图6-6-12 输入kill信号,杀死hello进程》
6.7本章小结
本章主要讲述了从可执行文件到进程的过程。首先介绍了shell的处理流程和作用,它是一个命令解释器,负责解释用户输入的命令并将其发送到内核。然后介绍了fork函数和execve函数,这两个函数在进程的创建和程序加载执行过程中起着重要的作用。fork函数用于创建子进程,子进程得到与父进程相同但独立的副本,包括代码和数据段、堆、共享库以及用户栈。execve函数则用于加载并运行一个新的可执行文件,它接收可执行文件的路径、参数列表和环境变量作为参数,并在当前进程的上下文中执行该程序。最后还介绍了上下文切换机制,当系统中有多个进程需要运行时,处理器会进行进程之间的切换,使得不同进程中的指令能够交替执行。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1. 逻辑地址:程序经过编译后出现在汇编代码中的地址。
2. 线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式,分页机制中线性地址作为输入。
3. 虚拟地址:也就是线性地址。
4. 物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成:段标识符和段内偏移量。段标识符是一个16位字段,也称为段选择符。通过段标识符的前13位,可以直接在段描述符表中找到对应的段描述符,从而描述一个段。
索引号即为段描述符在段描述符表中的索引,每个段描述符占据8个字节。全局的段描述符存放在全局段描述符表(GDT)中,而一些局部的段描述符则存放在局部段描述符表(LDT)中。
Linux使用分段机制将逻辑地址转换为线性地址。给定一个完整的逻辑地址[段选择符:段内偏移地址],首先判断段选择符的T1位是0还是1,以确定当前要转换的是GDT中的段还是LDT中的段,并根据相应的寄存器获取对应的地址和大小信息,形成一个数组。然后,通过段选择符的前13位,在这个数组中查找到相应的段描述符,从而获取基地址。最后,将基地址与偏移量相加,得到要转换的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(VA)到物理地址(PA)的转换是通过分页机制实现的。分页机制将虚拟地址空间划分为多个页面。
类似于在缓存中查找内容需要索引一样,从虚拟内存到物理内存也需要索引。因此,在内存中额外存储一个称为页表的数据结构,用作对应的索引。每个进程都有一个页表,页表中的每一项记录着进程中每一页所映射到的物理地址、有效性以及其他相关信息。
然而,由于页的大小为4KB,而虚拟内存大小为4GB,导致页表项数量达到2^20项,占用空间过大,并且许多页表项实际上是空的,因为大多数进程并未占用大量地址空间。因此,系统采用了多级页表的结构来进行索引。
系统将虚拟页作为数据传输的单元。在Linux中,每个虚拟页大小为4KB。物理内存也被划分为物理页,内存管理单元(MMU)负责地址转换,使用页表将虚拟页映射到物理页,即实现虚拟地址到物理地址的映射。
每次进行虚拟地址到物理地址的转换,都需要查询页表来判断虚拟页是否缓存在DRAM的某个位置。如果虚拟页不在DRAM中,则通过查询页表项可以得知虚拟页在磁盘上的位置。页表将虚拟页映射到物理页。页表是由页表项数组组成,每个页表项由有效位和n位地址字段组成。有效位表示虚拟页是否缓存在DRAM中,n位地址字段表示物理页的起始地址或虚拟页在磁盘上的起始地址。
虚拟地址的n位包含两部分:p位的虚拟页面偏移(VPO)和n-p位的虚拟页号(VPN)。MMU利用VPN选择相应的页表项,例如VPN 0选择PTE 0。根据页表项,我们可以了解虚拟页的信息。如果虚拟页已缓存,直接将页表项的物理页号和虚拟地址的VPO连接在一起,就得到相应的物理地址。这里的VPO和PPO是相同的。如果虚拟页未缓存,将触发页面错误。调用页面错误处理程序将磁盘上的虚拟页重新加载到内存中,然后再执行导致页面错误的指令。
7.4 TLB与四级页表支持下的VA到PA的变换
页表技术虽然能让我们再给出虚拟地址的时候,很大概率通过查找页表来找到内存地址,但是查页表也是访问内存的过程,也很浪费时间。利用局部性原理,像缓存一样,将最近使用过的页表项专门缓存起来。因此出现了TLB(后备转换缓冲器,也叫快表),之后找页表项的时候,先从快表找,找不到在访问内存中的页表项。
同理,四级页表能保证页表项的数量少一些。
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第51张图片](http://img.e-com-net.com/image/info8/08627052762240b2b66a84ff4c66efb2.jpg)
《图7-4-1 Core i7地址翻译的概况》
7.5 三级Cache支持下的物理内存访问
类似于TLB,利用局部性原理,缓存(Cache)以一种组相联(Set-Associative)的方式存储了最近加载的地址附近的数据。当获取到物理地址后,首先从L1缓存中查找,如果没有找到,则继续在L2缓存中查找,然后是L3缓存,最后才是主存。
这种层次化的缓存结构能够提供更快的数据访问速度,因为缓存位于处理器内部,而主存则位于处理器外部。L1缓存是最快的,但容量较小;L2缓存容量更大,速度稍慢;L3缓存则更大一些,但速度相对较慢;而主存容量最大,但速度最慢。因此,当数据不在较高级别的缓存中时,处理器需要从较低级别的缓存或主存中获取数据,这样的过程称为缓存的层次化访问。
通过在处理器内部使用缓存,可以减少对主存的频繁访问,从而提高处理器的性能。这种缓存机制充分利用了程序的局部性原理,即数据访问往往集中在一小部分地址附近,因此缓存能够有效地存储并快速提供这些频繁访问的数据,减少了处理器访问主存的延迟。
7.6 hello进程fork时的内存映射
在shell中输入命令./hello后,内核调用fork函数创建子进程,为hello程序的运行创建上下文,并分配一个与父进程不同的唯一的PID。为了给子进程创建虚拟内存,创建了当前进程的 mm_struct、区域结构和页表的原样副本。将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。
加载并运行 hello 需要以下几个步骤:
·删除当前进程虚拟地址中已存在的用户区域
·映射私有区域,为新程序的代码、数据、bss和栈创建新的区域结构,所有这些新的区域都是私有的、写时复制的。
·映射共享区域,将hello与libc.so动态链接,然后再映射到虚拟地址空间中的共享区域。
·设置当前进程上下文程序计数器(PC),使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页其实就是DRAM缓存未命中。当我们的指令中对取出一个虚拟地址时,若我们发现对该页的内存访问是合法的,而找对应的页表项式发现有效位为0,则说明该页并没有保存在主存中,出现了缺页故障。
此时进程暂停执行,内核会选择一个主存中的一个牺牲页面,如果该页面是其他进程或者这个进程本身页表项,则将这个页表对应的有效位改为0,同时把需要的页存入主存中的一个位置,并在该页表项储存相应的信息,将有效位置为1。然后进程重新执行这条语句,此时MMU就可以正常翻译这个虚拟地址了。
7.9动态存储分配管理
计算机动态存储分配管理是指操作系统如何有效管理存储器和磁盘空间。 这些管理分配了过程运行所需的内存和磁盘空间,回收了未使用的资源,允许用于其他过程或数据。
动态存储分配管理包括以下主要概念和方法:
1.虚拟内存:虚拟内存是一种为过程提供大于实际物理内存虚拟地址空间的技术。 过程要求虚拟内存达到所需的数量,操作系统只将需要的部分加载到实际内存中。 这样可以同时运行多个过程,并且可以运行大于物理内存大小的过程。
2.换页:换页是虚拟存储器中以页为单位存储和管理数据的一种技术。 当实际内存空间不足时,操作系统会将未使用的页面切换到磁盘上,并将所需的页面加载到内存中。 使用页面替换算法(如LRU、FIFO、Clock等)确定替换哪个页面。
3.档案系统:档案系统是组织磁盘空间和管理档案的技术。 文件系统处理文件的生成、删除、读写等,以块为单位保存文件并进行索引,提供高效的访问。 文件系统还维护了文件的安全性、权限管理、文件系统的一致性等。
4.动态分配:动态分配是流程在运行过程中动态分配所需存储器的技术。 流程向操作系统索取内存,操作系统分配可用内存空间提供给流程。 动态分配在过程运行时间对内存需求发生动态变化时是有用的,分配的内存应适当释放,避免出现内存泄漏等问题。
这些动态存储分配管理技术优化了系统的资源效率和性能,在多过程、多用户环境中稳定地管理操作系统具有重要作用。
7.10本章小结
本章主要讲述了"hello"程序的存储器地址空间以及逻辑地址到线性地址、线性地址到物理地址的转换过程。接着,介绍了在四级页表下线性地址到物理地址的转换方式。分析了"hello"程序的内存映射,以及缺页故障和缺页中断的处理过程,还有动态存储分配管理。
"hello"程序的存储器地址空间是指程序运行时所使用的内存范围。通过逻辑地址到线性地址的转换,程序的逻辑地址可以被转换为对应的线性地址。接着,线性地址再经过转换,可以得到物理地址,即真实的内存地址。在使用四级页表的情况下,线性地址到物理地址的转换涉及到多级页表的查找过程。
"hello"程序的内存映射是指程序的代码、数据和堆栈等部分在内存中的分布情况。通过合理的内存映射,程序可以被正确加载和执行。当发生缺页故障时,即访问的页面不在内存中时,会触发缺页中断,操作系统会通过缺页中断处理程序将缺失的页面加载到内存中,然后再继续执行程序。
动态存储分配管理是指操作系统在运行时根据程序的需求进行内存的分配和释放。动态存储分配管理技术可以有效地管理内存资源,避免内存浪费和内存溢出的问题,提高系统的性能和资源利用率。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
在Linux中,I/O(subsystem)设备是内核的核心功能之一,它管理着与输入输出设备的相互作用。 I/O设备管理对维护系统的效率和稳定性起着重要作用。
在Linux中,I/O设备管理方法包括以下主要因素:
设备驱动程序:提供I/O设备与内核之间接口的软件模块。 设备驱动程序由内核模块实现,对每种设备类型进行特定操作。 设备驱动程序处理输入输出请求,管理与设备的通信。
设备文件系统: Linux 将所有设备视为文件。 因此,对I/O设备的访问是通过文件系统进行的。 设备文件系统生成设备文件,并分配文件表单,使过程能够与设备交互。
输入输出调度:由于输入输出操作需要大量的系统资源,因此需要高效的调度。 输入输出调度根据优先顺序调整各种输入输出请求,优化系统性能。 常见的输入输出调度算法有FCFS(First-Come,First-Served)、SSTF(Shortest Seek Time First)、SCAN、C-LOOK等。
缓冲缓存:为了优化I/O操作,Linux采用缓冲缓存技术。 缓冲是一种通过批量处理输入输出操作来提高效率的方法,缓存将经常访问的数据保留在存储器中,从而缩短访问时间。 这些技术有助于提高输入输出性能,减少系统负荷。
Linux的I/O设备管理对系统的稳定性和性能有很大的影响。 因此,Linux内核利用设备驱动程序、设备文件系统、输入输出调度、缓冲、缓存等多种技术和机制,实现与输入输出设备的有效交互。
8.2 简述Unix IO接口及其函数
8.2.1 Unix I/O 接口
(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想 要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在 后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
(2)Linux Shell创建的每个进程都有三个打开的文件:标准输入、标准输出、标准错误。
(3)改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。
(4)读写文件。一个读操作就是从文件复制 n > 0 个字节到内存,从当前文件位置 k 开始,然后将 k 增加到 k + n。给定一个大小为 m 字节的文件,当 k >= m 时,执行读操作会触发 EOF,应用程序能检测到它。类似地,写操作就是从内存中复制 n > 0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。
(5)关闭文件。内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
8.2.2函数
(1) open函数:int open(char *filename,int flags,mode_t node);
将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags 参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
(2) close函数:int close(int fd);
关闭一个打开的文件,当关闭已关闭的描述符会出错。
(3) read函数:ssize_t read(int fd,void *buf,size_t n);
从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。
(4) write函数:ssize_t write(int fd,const void *buf,size_t n);
从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
(5) lseek函数:off_t lseek(int fd, off_t offset, int whence);
应用程序显示地修改当前文件的位置。
(6) stat函数:int stat(const char *filename,struct stat *buf);
以文件名作为输入,并填入一个stat数据结构的各个成员。
8.3 printf的实现分析
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第52张图片](http://img.e-com-net.com/image/info8/d5f24bb03f714097b4637947065914c0.png)
《图8-3-1 printf函数的函数体》
在红线中,第一行目的是让argv指向第一个字符串;第二句的作用是格式化,并返回要打印的字符串的长度,第三句的作用是调用write函数将buf的前i个字符输出到终端,调用了unix I/O。
官方解释:
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar的源代码如下:
![【2023】哈工大计算机系统大作业——程序人生Hello’s P2P_第53张图片](http://img.e-com-net.com/image/info8/7a19716ebbd04d2da694a1bcadac7ca2.jpg)
《图8-4-1 getchar源代码》
getchar函数内部调用了read函数来读取存储在键盘缓冲区中的ASCII码,直到读取到回车符(Enter键)时才返回。但是,每次调用read函数时,它会将缓冲区中的所有内容读取进来。如果缓冲区本身就是非空的,则不会调用read函数,而是直接返回缓冲区中最前面的元素。
异步异常-键盘中断的处理是指键盘中断处理子程序。它接收按键的扫描码并将其转换为ASCII码,然后将其保存到系统的键盘缓冲区中。
8.5本章小结
本章主要介绍了Linux的I/O设备管理方法、Unix IO接口及其函数,并分了printf函数和getchar函数的实现。
结论
一开始,程序员逐字地输入hello.c代码,然后轻松地点击运行,"Hello, World!"就出现在屏幕上。虽然看起来很简单,但是在深入了解之后才发现,这一切并不那么简单。
首先,hello.c程序静静地保存在磁盘中,等待执行。然后,hello.c经过预处理,头文件被引入,宏被展开等操作,生成了hello.i文件。接下来,编译器对hello.i进行处理,生成了hello.s汇编文件,再经过汇编器的转换,最终变成了机器能理解的二进制代码文件hello.o。但是这还不是结束,hello.o文件还需要进行链接,最终生成可执行文件hello。
当可执行文件生成后,为了执行它,程序员在终端输入./hello。虽然结果瞬间就出来了,但是其中经历了许多步骤。
首先,shell解析命令行输入的命令,然后调用fork创建子进程,并使用execve将可执行文件映射到虚拟内存中。当CPU执行到hello时,开始读取对应的虚拟内存地址,并通过缺页异常将hello加载到主存中。然后通过四级页表、各级缓存等逐步加载,最终hello被加载到处理器内部。
接着,通过I/O函数进行输入输出操作,最终结果被输出到终端。
最后,hello.c程序被回收,重新返回到硬盘上。
虽然hello的生命周期非常短暂,但是却经历了许多曲折而精彩的过程!
计算机系统这门课程总体而言,涉及的知识很广,难度也不小,但非常有趣。计算机系统比我想象的要复杂得多,其中隐藏着许多奥秘。通过学习这门课程,通过实际进行实验和查阅资料,我学到了许多新知识,提高了学习能力。
总的来说,计算机系统这门课程让我受益匪浅,我将继续学习相关知识。
附件
hello.i:hello.c预处理后的文件。
hello.s:hello.i编译后的文件。
hello.o:hello.s汇编后的文件。
hello:hello.o链接后的文件。
hello1asm.txt:hello.o反汇编后代码。
hello2asm.txt:hello反汇编后代码。
hello.o_elf:hello.o用readelf -a hello.o指令生成的文件。
hello_elf:hello用readelf -a hello指令生成的文件。
参考文献
[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.