2021计算机系统大作业 CSAPP&Hello‘s P2P

本文详细剖析了从C语言源码到可执行程序的全程,涵盖预处理、编译、汇编、链接、进程管理、存储管理和IO管理,通过hello.c程序实例深入浅出地讲解了计算机底层原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

摘 要

本文基于CSAPP和计算机系统基础课程中的内容,通过hello.c这一简单程序的生命周期,讨论了计算机中的预处理、编译、汇编、链接、进程管理、存储管理、IO管理等内容。

关键词: 计算机;编译;进程;存储管理;IO管理

目 录

第1章 概述 - 6 -

1.1 Hello简介 - 6 -

1.1.1 Hello的P2P过程 - 6 -

1.1.2 Hello的020过程 - 6 -

1.2 环境与工具 - 6 -

1.2.1硬件环境 - 6 -

1.2.2软件环境 - 6 -

1.2.3开发与调试工具 - 7 -

1.3 中间结果 - 7 -

1.4 本章小结 - 7 -

第2章 预处理 - 8 -

2.1 预处理的概念与作用 - 8 -

2.1.1 预处理的概念 - 8 -

2.1.2 预处理的作用 - 8 -

2.1.2.1 宏定义与拓展 - 8 -

2.1.2.2 文件包含 - 8 -

2.1.2.3 条件编译 - 8 -

2.1.2.4 行控制 - 8 -

2.1.2.5 错误信息生成 - 9 -

2.1.2.6 编译指示 - 9 -

2.2在Ubuntu下预处理的命令 - 9 -

2.3 Hello的预处理结果解析 - 9 -

2.4 本章小结 - 10 -

第3章 编译 - 11 -

3.1 编译的概念与作用 - 11 -

3.1.1 编译的概念 - 11 -

3.1.2 编译的作用 - 11 -

3.2 在Ubuntu下编译的命令 - 12 -

3.3 Hello的编译结果解析 - 12 -

3.3.1整体解析 - 12 -

3.3.2数据存储 - 13 -

3.3.2.1常量 - 13 -

3.3.2.2局部变量 - 13 -

3.3.3赋值及算术语句 - 14 -

3.3.4选择及循环控制语句 - 14 -

3.3.4.1选择语句 - 14 -

3.3.4.2循环语句 - 15 -

3.3.5.1 main函数 - 15 -

3.3.5.2 atoi及sleep函数 - 16 -

3.3.5.3 printf函数 - 17 -

3.3.5.4 exit函数 - 17 -

3.4 本章小结 - 17 -

第4章 汇编 - 18 -

4.1 汇编的概念与作用 - 18 -

4.1.1汇编的概念 - 18 -

4.1.2汇编的作用 - 18 -

4.2 在Ubuntu下汇编的命令 - 18 -

4.3 可重定位目标elf格式 - 18 -

4.4 Hello.o的结果解析 - 22 -

4.5 本章小结 - 23 -

第5章 链接 - 24 -

5.1 链接的概念与作用 - 24 -

5.1.1链接的概念 - 24 -

5.1.2链接的作用 - 24 -

5.2 在Ubuntu下链接的命令 - 24 -

5.3 可执行目标文件hello的格式 - 25 -

5.4 hello的虚拟地址空间 - 27 -

5.5 链接的重定位过程分析 - 28 -

5.6 hello的执行流程 - 29 -

5.7 Hello的动态链接分析 - 29 -

5.8 本章小结 - 29 -

第6章 hello进程管理 - 30 -

6.1 进程的概念与作用 - 30 -

6.1.1进程的概念 - 30 -

6.1.2进程的作用 - 30 -

6.2 简述壳Shell-bash的作用与处理流程 - 30 -

6.2.1 shell-bash的作用 - 30 -

6.2.2 shell-bash的处理流程 - 30 -

6.3 Hello的fork进程创建过程 - 31 -

6.4 Hello的execve过程 - 31 -

6.5 Hello的进程执行 - 31 -

6.6 hello的异常与信号处理 - 32 -

6.6.1 异常与信号处理过程 - 32 -

6.6.2 各种命令的执行 - 32 -

6.7本章小结 - 34 -

第7章 hello的存储管理 - 35 -

7.1 hello的存储器地址空间 - 35 -

7.2 Intel逻辑地址到线性地址的变换-段式管理 - 35 -

7.3 Hello的线性地址到物理地址的变换-页式管理 - 35 -

7.4 TLB与四级页表支持下的VA到PA的变换 - 36 -

7.5 三级Cache支持下的物理内存访问 - 37 -

7.6 hello进程fork时的内存映射 - 37 -

7.7 hello进程execve时的内存映射 - 38 -

7.8 缺页故障与缺页中断处理 - 38 -

7.8.1 缺页故障 - 38 -

7.8.2 缺页中断处理 - 39 -

7.9动态存储分配管理 - 39 -

7.9.1 动态内存管理的基本方法 - 40 -

7.9.2 动态内存管理的策略 - 40 -

7.10本章小结 - 40 -

第8章 hello的IO管理 - 42 -

8.1 Linux的IO设备管理方法 - 42 -

8.2 简述Unix IO接口及其函数 - 42 -

8.2.1 Unix I/O简介 - 42 -

8.2.2 open/close函数 - 42 -

8.2.3 read/write函数 - 42 -

8.2.4 lseek函数 - 43 -

8.2.5 stat函数 - 43 -

8.3 printf的实现分析 - 43 -

8.4 getchar的实现分析 - 44 -

8.5本章小结 - 44 -

结论 - 45 -

附件 - 46 -

参考文献 - 47 -

第1章 概述

1.1 Hello简介

1.1.1 Hello的P2P过程

程序员通过外部IO设备键盘将程序输入到文本编辑器中,这个过程就是编程(program),随后C语言预处理器对该文件进行预处理,得到Hello.i文件;编译器对Hello.i文件进行编译,得到汇编语言文件Hello.s;汇编器将Hello.s文件汇编为可重定位目标文件Hello.o,最后再由链接器生成可执行文件Hello。

程序员打开操作系统中的终端(shell),运行./Hello指令,shell对输入的指令进行处理,通过fork生成子进程,在子进程中进行execve运行Hello程序。操作系统为Hello程序分配时间片,Hello得以运行。这就是process。

From program to process,这就是Hello的P2P过程。

1.1.2 Hello的020过程

Shell的execve过程会先删除当前虚拟地址的用户部分已存在的数据结构,为hello创建新的区域结构,然后映射共享区域,设置程序计数器,映射虚拟内存,最后加载物理内存。Hello开始运行。

当Hello执行结束后,shell父进程负责回收该子进程,删除其占用的内存,内核也删除相关的数据结构,shell继续等待下一个命令的输入。

从无到无,from zero to zero,此即Hello的020过程。

1.2 环境与工具

1.2.1硬件环境

处理器:Intel® Core™ i5-8265U CPU @ 1.60GHz 1.80GHz

内存:16.0GB 三星DDR4 2666MHz

硬盘:256G固态(系统盘)+三星500G固态

显卡:英伟达MX250 2GB

1.2.2软件环境

Win10 + Vmware15.5.0 + Ubuntu 20.04

1.2.3开发与调试工具

Win: vscode + mingw

Linux:vscode + codeblocks + gcc + dgb + edb

1.3 中间结果

文件名称 文件描述


hello.c 老师提供的C语言源代码
hello.i hello.c预处理后生成的文件
hello.s hello.i编译后生成的汇编语言文件
hello.o hello.s汇编后生成的可重定位目标文件
hello hello.o链接后生成的可执行目标文件
hello_o_elf.txt hello.o文件的ELF格式
hello_elf.txt hello文件的ELF格式
hello_o_disassembly.s hello.o反汇编得到的文件
hello_disassembly.s hello反汇编得到的文件

表1-1 实验中间结果

1.4 本章小结

本章第一节根据PPT中hello的自白简述了hello
P2P,O2O的过程,第二节介绍了做实验时的软硬件环境和所使用的工具,第三节列出了实验生成的中间文件。

第2章 预处理

2.1 预处理的概念与作用

2.1.1 预处理的概念

C语言预处理器(cpp)根据以未进行过宏扩展的以字符#开头的命令,修改原始的C语言程序。

  1. 首先,将三字符序列替换为等价字符(gcc默认不替换)。如果操作系统环境需要,还要在源文件的各行之间插入换行符。

  2. 将指令行中位于换行符前的反斜杠符\删除掉,以把各指令行连接起来。

  3. 将程序分成用空白符分隔的记号。注释将被替换为一个空白符。接着执行预处理指令,并进行宏扩展。

  4. 将字符常量和字符串字面值中的转义字符序列替换为等价字符,然后把相邻的字符串字面值连接起来。

  5. 收集必要的程序和数据,并将外部函数和对象的引用与其定义相连接,翻译经过以上处理得到的结果,然后与其他程序和库连接起来。

2.1.2 预处理的作用

2.1.2.1 宏定义与拓展

格式:#define 标识符 记号序列 或 #define 标识符(标识符表) 记号序列
或#undef 记号序列

作用:使得预处理器把该标识符后续出现的各个实例用给定的记号序列替换。

2.1.2.2 文件包含

格式:#include 文件名

作用:把该行替换为文件名指定的文件的内容。

2.1.2.3 条件编译

格式:#if #elif #else #endif #ifdef #ifndef指令的组合。

作用:可以使用#ifndef … #define …
#endif来进行头文件保护(#include guard),可以使用#if
#ifdef等对程序进行条件编译,用于选择软件版本、判断操作系统环境等。

2.1.2.4 行控制

格式:#line常量 “文件名” 或 #line 常量

作用:给出下一行源代码的行号,若含有"文件名",则一并给出当前编译的源文件的名字,有利于我们对错误进行诊断。

2.1.2.5 错误信息生成

格式:#error 记号序列

作用:使预处理器打印包含该记号序列的诊断信息。

2.1.2.6 编译指示

格式:#pragma 记号序列

作用:使预处理器执行一个与具体实现相关的操作,无法识别的pragma将被忽略掉。

2.2在Ubuntu下预处理的命令

命令:gcc -E hello.c -o hello.i在这里插入图片描述

图2-1 预处理生成hello.i
 

2.3 Hello的预处理结果解析

用vscode打开hello.c与hello.i,如图2-2所示,左半部分是hello.c的内容,右半部分是hello.i的部分内容。
在这里插入图片描述

图2-2 hello.c与hello.i对比

 

从内容上对比,hello.i比hello.c多了很多库的信息,少了#include和注释。结合2.1.1条中预编译的步骤,可以知道,C语言预处理器将注释替换成了空白符,并将程序和库连接了起来。

2.4 本章小结

本章第一节介绍了C语言预处理的概念和作用,第二节在ubuntu中使用gcc进行预处理操作,并在第三节中将预处理生成的文件和原C语言文件比较,实际分析了预处理的结果。

第3章 编译

3.1 编译的概念与作用

3.1.1 编译的概念

编译器是将高级语言程序解释成为计算机所需的详细机器语言指令集的程序。其工作过程主要分为五个阶段:词法分析,语法分析,语义检查和中间代码生成,代码优化,目标代码生成。

1、词法分析:

词法分析的任务是对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。

2、语法分析:

编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构,程序是最终的一个语法单位。

3、中间代码:

中间代码是源程序的一种内部表示,或称中间语言。中间代码的作用是使编译程序的结构在逻辑上更为简单明确。特别是可使目标代码的优化比较容易实现中间代码,即为中间语言程序,中间语言的复杂性介于源程序语言和机器语言之间。

4、代码优化:

代码优化是指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。

5、目标代码:

目标代码生成是编译的最后一个阶段。目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。

3.1.2 编译的作用

C语言编译器将C预处理器处理过后的文件翻译成汇编语言文件。它主要进行词法分析和语法分析,分析过程中若发现有错误,编译器会报告错误的性质和错误的发生地点,并且将错误所造成的影响限制在尽可能小的范围内,使得源程序的其余部分能继续被编译下去。

3.2 在Ubuntu下编译的命令

命令:gcc -S hello.i -o hello.s
在这里插入图片描述

图3-1 编译生成hello.s

 

3.3 Hello的编译结果解析

3.3.1整体解析

首先,我先将hello.s中的内容全部注释了一遍,如图3-2所示。3.3节的下面几条将会分方面解析编译结果。
在这里插入图片描述

a\)

在这里插入图片描述

b\)
图3-2 hello.s注释

 

3.3.2数据存储

3.3.2.1常量

在hello.c中,printf函数传入的参数"用法:Hello 学号 姓名 秒数!\n"和
“Hello %s %s\n” 均为字符串常量,常量被存储在.rodata中,如图3-3所示。
在这里插入图片描述

图3-3 字符串常量的存储

 

3.3.2.2局部变量

整型变量argc和i,二维数组argv都是局部变量,局部变量被存储在栈中。分析汇编程序可知,argc被存储在(rbp-20)地址中,i被存储在(rbp-4)地址中,argv被存储在(rbp-32)地址中,如图3-4所示。
在这里插入图片描述

图3-4 局部变量的存储

 

3.3.3赋值及算术语句

在hello.s中使用了add,mov,sub等赋值和算术语句。ADD S, D 表示D =
D+S;SUB S, D 表示 D = D-S;MOV S, D 表示D =
S。图3-5中展示了几处赋值及算术语句。
在这里插入图片描述

图3-5 赋值及算术语句

 

其中,subq $32, %rsp表示将rsp寄存器中的内容减去立即数32。movl %edi,
-20(%rbp) 表示将edi寄存器中的内容存放在(rbp-20)地址中。

3.3.4选择及循环控制语句

3.3.4.1选择语句

使用比较函数判断两个值的大小,再使用跳转函数进行跳转,就实现了C语言中各种选择语句。

比如在hello.c中有着if(argc!=4)这条语句,在hello.s中就对应翻译成了图3-6中的内容。
在这里插入图片描述

图3-6 选择语句

 

其中(rbp-20)地址中存放的是argc变量,cmpl将argc与4比较,若等于4则跳转到L2继续执行,若不等于4就执行printf函数和exit函数,程序退出。

3.3.4.2循环语句

hello.c中存在一个for循环语句:for(i=0;i<8;i++),其在hello.s中被翻译成了图3-7中的内容。
在这里插入图片描述

图3-7 循环语句

 

(rbp-4)地址中存放的是局部变量i,addl位于L4中,每次将i加1,在L3中将i和7进行比较,如果小于等于7就跳转回L4,继续执行循环,如果大于7就跳出循环,继续往下执行。

3.3.5.1 main函数

hello.c中涉及的函数有main,printf,atoi,sleep,exit函数。main函数由操作系统调用,这里我们能看到main函数传入的参数及main函数的返回,分别如图3-8和图3-9所示。
在这里插入图片描述

图3-8 main函数传入参数

 
在这里插入图片描述

图3-9 main函数返回

 

edi寄存器中存放的是main函数传入的argc参数,esi寄存器中存放的是main函数传入的argv参数。main函数结束后使用leave和ret指令返回。

3.3.5.2 atoi及sleep函数

图3-10展示的是atoi及sleep函数的调用,首先将argv[3]字符串的地址存入rdi寄存器,再使用call命令调用atoi函数,并将rdi寄存器中的内容传入atoi函数。

随后atoi函数的返回值被存储在eax寄存器中,再使用movl指令将eax寄存器中的内容存储在edi寄存器中,最后调用sleep函数,实现了C语言中的sleep(atoi(argv[3]))。
在这里插入图片描述

图3-10 atoi及sleep函数的调用

 

3.3.5.3 printf函数

首先使用leaq命令将字符串常量的地址复制到rdi寄存器,随后使用call函数调用puts函数,我猜测这里没有调用printf函数是因为这里只输出了一个字符串常量,没有其他参数的调用,所以编译器将printf优化为puts,见图3-11。
在这里插入图片描述

图3-11 第一次调用printf函数

 

通过一系列指令,将字符串常量"Hello %s
%s\n",argv[1]和argv[2]传入printf函数,最后调用printf函数。
在这里插入图片描述

图3-12 第二次调用printf函数

 

3.3.5.4 exit函数

如图3-13所示,通过将立即数1复制到edi寄存器,再使用call调用exit函数,实现了C语言的exit(1)。
在这里插入图片描述

图3-13 调用exit函数

 

3.4 本章小结

本章第一节分析了编译的概念和作用,第二节在ubuntu中将hello.i编译生成hello.s汇编语言文件,第三节首先整体解析了hello.s,然后从数据存储、赋值及运算语句、选择及循环控制语句和函数调用四个部分详细地分析了hello.s,阐明了C语言和汇编语言之间的对应关系。

第4章 汇编

4.1 汇编的概念与作用

4.1.1汇编的概念

汇编指将汇编语言翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将保存结果保存在在.o的二进制文件中。

4.1.2汇编的作用

汇编的作用是将汇编指令根据汇编指令和机器指令的对照表一一翻译转换成机器可以直接读取分析的机器指令。

4.2 在Ubuntu下汇编的命令

命令:gcc -c hello.s -o hello.o 或 as hello.s -o hello.o
在这里插入图片描述

a\)

在这里插入图片描述

b\)
图4-1 汇编生成hello.o

 

4.3 可重定位目标elf格式

首先使用readelf工具生成hello_elf.txt文件,并用vscode打开,见图4-2。
在这里插入图片描述

图4-2 使用readelf工具分析hello.o

 

如图4-3所示,hello_elf.txt文件的第一部分是ELF头,ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。

然后ELF头还包括很多帮助连接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、CPU的架构、操作系统和数据存储的方式等。可以看到,操作系统是UNIX
– System V,CPU架构是Advanced Micro Devices
X86-64,数据存储方式是补码和小端序,hello.o ELF头的大小是64字节。
在这里插入图片描述

图4-3 elf头

 

如图4-4所示,ELF头后面的内容是节头,包含了各节的名称、类型、地址、偏移量、大小、全体大小、旗标、链接、对齐等信息。
在这里插入图片描述

图4-4 节头表

 

如图4-5所示,节头表后面是各个节,首先是重定位代码节(.rela.text)和.rela.eh_frame节。

.rela.text是一个.text节中位置的列表,当连接器把hello.o和其他文件组合时,需要修改这些位置。一般来说,任何调用外部函数或者引用全局变量的指令都需要修改,另一方面,调用本地函数的指令则不需要修改。从图4-5中可以看出,hello.o中的.rela.text节包含了puts、exit、printf、atoi、sleep、getchar函数的重定位位置。
在这里插入图片描述

图4-5 重定位节

 

如图4-6所示,重定位节后面是符号表,它存放在hello.c中定义和引用的函数和全局变量的信息,比如main、puts、exit等函数。
在这里插入图片描述

图4-6 符号表

 

4.4 Hello.o的结果解析

使用objdump工具将hello.o反汇编,并将输出重定向至hello_disassembly.s文件中,然后使用vscode打开,如图4-7所示。
在这里插入图片描述

图4-7 hello.o反汇编

 

机器语言由很多十六进制代码组成,如图4-8左半部分所示,可以看到,每个汇编指令都被翻译成了对应的机器指令。
在这里插入图片描述

图4-8 反汇编结果

 

将反汇编生成的hello_disassembly.s与hello.s比较,如图4-9所示,可以看到:反汇编文件中所有跳转都被替换成了实际地址,而汇编语言文件是通过label来实现的,这是因为hello.c中的跳转均在函数内部,所以不需要链接,汇编器也可以确定跳转地址间的相对偏移量,故地址已经确定。

还有汇编语言的文件使用call指令时是直接通过call+函数名调用,不需要进行寻址,但是反汇编文件中所有call指令后都预留了4个字节的占位符,指向的是下一条地址的位置,等待链接后再重定位为具体的函数位置。
在这里插入图片描述

图4-9 反汇编与hello.s比较

 

4.5 本章小结

本章第一节分析了汇编的概念和作用,第二节在ubuntu中将汇编语言文件hello.s汇编生成可重定位文件hello.o,第三节使用readelf工具分析了hello.o文件的ELF格式,并分析了每一部分内容的含义,第四节使用objdump工具将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

见图5-1。
在这里插入图片描述

图5-1 使用ld链接生成可执行文件

 

其中,ld-linux-x86_64.so.2是链接器ld本身所依赖的库,crt1.o、crti.o和crtn.o是C运行时所依赖的环境,libc.so中包含了printf、getchar、exit等函数的定义。

所以只要把我们编写的hello.o和这三个可重定位文件和这两个动态库链接到一起,就可以生成可执行文件hello了。

5.3 可执行目标文件hello的格式

之前在分析hello.o时生成了hello_elf.txt文件,现在将其改名为hello_o_elf.txt,然后使用readelf分析可执行文件hello,并将输出重定位至hello_elf.txt,最后使用vscode打开,终端命令如图5-2所示。
在这里插入图片描述

图5-2 使用readelf工具分析hello

 

由于在4.3节已经分析过可重定位目标文件hello.o的ELF格式了,而可重定位目标文件和可执行目标文件的ELF格式类似,所以这里只分析和4.3节不同的内容。

hello各段的名称、类型、地址、偏移量、大小、全体大小、旗标、链接、对齐等信息见图5-3。
在这里插入图片描述

a\)

在这里插入图片描述

b\)
图5-3 hello节头表

 

相比hello.o文件,hello多了程序头,包括偏移量、虚拟地址、物理地址、段的大小等信息,如图5-4所示。
在这里插入图片描述

图5-4 程序头

5.4 hello的虚拟地址空间

使用edb加载hello,在View–Memory
Regions页面中选择想要查看的虚拟内存,然后在Data
Dump中查看,如图5-5所示。

图5-5中红色圈圈中部分的虚拟地址为0x0000000000400040,与5.3节中的图5-4中的PHDR段的VirtAddr相同。
在这里插入图片描述

图5-5 hello虚拟地址空间

 

5.5 链接的重定位过程分析

和5.3节的情况类似,之前在分析hello.o时生成了hello_disassembly.s文件,现在将其改名为hello_o_disassembly.s,然后使用objdump反汇编可执行文件hello,并将输出重定位至hello_disassembly.s,最后使用vscode打开,终端命令如图5-6所示。
在这里插入图片描述

图5-6 使用objdump工具反汇编hello

 

将hello_disassembly.s与hello_o_disassembly.s对比,如图5-7所示。我们可以发现,hello.o反汇编的结果只有.text段的内容,但是经过链接后生成的hello文件反汇编的结果有.init,.plt,.text等段的内容,并且例如puts、printf、sleep等函数也有了在虚拟内存中具体的地址,主函数main也是如此,不像hello_disassembly.s中main函数的虚拟地址是0。
在这里插入图片描述

图5-7 hello_disassembly.s与hello_o\_disassembly.s对比

 

这说明链接时会将全局符号(包括全局变量和函数)进行重定位,赋予绝对的虚拟地址,程序在执行的过程中可以直接访问这些地址来调用函数。

5.6 hello的执行流程

在这里插入图片描述

图5-8 hello的执行流程

 

5.7 Hello的动态链接分析

5.8 本章小结

本章第一节简述了链接的概念及作用,第二节展示了链接hello的命令,第三节使用了readelf工具对hello可执行文件进行分析,第四节使用edb工具查看hello的虚拟地址空间,并与第三节中得到的信息进行比对,第五、六、七节分别分析了hello重定位、执行和动态链接的过程。

第6章 hello进程管理

6.1 进程的概念与作用

6.1.1进程的概念

进程的经典定义就是一个执行中程序的实例,系统中的每个程序都运行在某个进程的上下文中。

6.1.2进程的作用

进程给应用程序提供了两个关键抽象:

1.
一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。

2.
一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。

6.2 简述壳Shell-bash的作用与处理流程

6.2.1 shell-bash的作用

首先,shell-bash
是一个程序,提供一个与用户对话的环境。这个环境只有一个命令提示符,让用户从键盘输入命令,所以又称为命令行环境。Shell
接收到用户输入的命令,将命令送入操作系统执行,并将结果返回给用户。

其次,shell-bash是一个命令解释器,解释用户输入的命令。它支持变量、条件判断、循环操作等语法,所以用户可以用
Shell 命令写出各种小程序,又称为脚本。这些脚本都通过
shell-bash的解释执行,而不通过编译。

最后,shell-bash是一个工具箱,提供了各种小工具,供用户方便地使用操作系统的功能。

6.2.2 shell-bash的处理流程

1.
shell-bash读入用户输入的命令,首先进行处理,替换掉某些环境变量,然后shell-bash会先检查该命令是否为shell内置命令,如果是,shell-bash会立即解释该命令,如果不是,shell-bash会认为用户输入的是一个可执行目标文件的文件名。

2. shell-bash使用fork这个系统调用函数创建了一个子进程

3.
fork函数被调用一次会返回两次,没有出错的情况下,在子进程中返回0,在父进程中返回子进程的pid。利用这一特性,shell-bash在子进程中调用execve函数,加载用户输入的可执行目标文件。

4.
如果用户输入的最后一个参数是&字符,则表示命令在后台执行,shell-bash回到最开始的状态,等待用户输入命令。如果没有&字符,shell-bash会调用wait函数来挂起自己,直到子进程结束为止。

5.
子进程结束后,shell-bash需要回收子进程,将子进程彻底释放,避免出现僵死进程,浪费系统资源。

6.3 Hello的fork进程创建过程

当检测到用户输入./hello后,shell-bash调用fork函数创建一个新的子进程。

fork创建的子进程得到与父进程用户级虚拟地址空间相同但是独立的一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本。fork生成的子进程与父进程并发执行,子进程和父进程有着不同的pid。

fork函数调用一次返回两次,在没有出错的情况下,在子进程中返回0,在父进程中返回子进程的pid,根据这一特点可以区分子进程和父进程,为随后的execve做准备。

6.4 Hello的execve过程

shell-bash调用完fork函数生成子进程后,在子进程中调用execve函数,执行可执行目标文件hello。

execve函数在当前进程的上下文中加载并运行一个新程序,除非发生错误,否则execve函数从不返回。

6.5 Hello的进程执行

当hello在执行时,操作系统中的调度器为hello进程分配了一个时间片,时间片一到,调度器就会调度hello进程,保存hello进程的上下文(上下文就是内核重新启动一个被抢占的进程所需的状态,包括各种寄存器和栈的值),并将hello进程挂起,切换到其他进程执行。

在hello中调用了sleep函数,显式地请求休眠,hello进程从用户模式切换到内核模式,在内核模式下进行系统调用,系统调用结束后,处理器将模式从内核模式改回到用户模式。

6.6 hello的异常与信号处理

6.6.1 异常与信号处理过程

hello的执行过程中一定会出现中断和系统调用(陷阱)、可能会出现故障和中止。

hello在接收键盘发出的信号(比如Ctrl+C)时会产生中断;hello调用sleep函数时发生了系统调用;除此之外,hello执行的过程中也可能出现故障和中止。

当键盘输入Ctrl+C时,会向hello进程发送一个SIGINT信号,默认情况下,hello进程会被终止。当键盘输入Ctrl+Z时,会向hello进程发送一个SIGTSTP信号,将进程挂起。当使用kill命令时,默认会向hello进程发送SIGTERM命令,进程会终止,但如果该信号被阻塞,进程也可能不受影响,常用的kill
-9命令会向进程发送SIGKILL信号,该信号无法被阻塞或者忽略,进程会被强制终止。

6.6.2 各种命令的执行

当输入Ctrl+C命令时,hello进程会被终止,如图6-1所示。
在这里插入图片描述

图6-1 Ctrl+C命令

 

当输入Ctrl+Z命令时,hello进程被挂起在后台,如图6-2所示。
在这里插入图片描述

图6-2 Ctrl+Z命令

 

hello进程被挂起之后执行ps
-all命令可以看到被挂起的hello进程,如图6-3所示。
在这里插入图片描述

图6-3 ps命令

 

jobs命令可以查看被挂起的进程,如图6-4所示。
在这里插入图片描述

图6-4 jobs命令

 

pstree命令用于查看当前的进程树,如图6-5所示。
在这里插入图片描述

图6-5 pstree命令

 

fg命令可以重新执行被挂起的hello进程,如图6-6所示。
在这里插入图片描述

图6-6 fg命令

 

首先使用ps
-all指令查看被挂起的hello进程的pid,随后使用kill命令,kill默认会向进程发送SIGTERM命令以终止该进程,但是该信号可以被阻塞,在我的测试中,该信号无法终止一个被挂起的进程。所以我加了-9参数,向hello进程发送SIGKILL信号,该信号无法被阻塞或者忽略,hello进程被杀死,如图6-7所示。
在这里插入图片描述

图6-7 kill命令

 

6.7本章小结

本章第一、二、三、四节分别阐述了进程的概念和作用、shell-bash的作用和处理流程,以及fork和execve函数的过程;第五节从上下文、时间片等角度分析了hello进程在操作系统中执行的过程;最后一节分析了各种异常和信号处理,并加以实验。

第7章 hello的存储管理

7.1 hello的存储器地址空间

物理地址是读写内存芯片时使用的地址。计算机系统的内存被组织成一个由M个连续的字节大小的单元组成的数组,每个字节都有一个唯一的物理地址。

虚拟地址是CPU生成的地址,CPU通过虚拟地址来访问内存,CPU将该虚拟地址送往内存管理单元MMU,MMU利用存放在内存中的查询表动态地将虚拟地址翻译为物理地址,然后进行内存的读写。

逻辑地址是指由程序产生的与段相关的偏移地址部分。

线性地址是逻辑地址经过段机制后形成的,其地址空间是一个非负整数地址的有序集合,如果地址空间中的整数是连续的,它是一个线性地址空间。

7.2 Intel逻辑地址到线性地址的变换-段式管理

在段式存储管理中,将程序的地址空间划分为若干个段,使得每个进程有一个二维的地址空间。段式存储管理系统为每个段分配一个连续的分区,而进程中的各个段可以不连续地存放在内存的不同分区中。

实模式下逻辑地址和线性地址相等,段寄存器存放真实段基址,同时给出32位地址偏移量,用来访问真实物理内存。

保护模式下使用段描述表的方式,通过16位段选择符来寻找基地址。

7.3 Hello的线性地址到物理地址的变换-页式管理

虚拟内存被组织为一个由存放在磁盘上的N个连续字节大小的单元组成的数组,每字节都有一个唯一的虚拟地址,作为到数组的索引。VM系统将虚拟内存分割为虚拟页。

页表是一个存放在物理内存中的数据结构,将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。操作系统负责维护页表中的内容,以及在磁盘与DRAM之间来回传送页。

为了压缩页表,计算机中使用层次结构的页表,一个典型的二级页表层次结构如图7-1所示。
在这里插入图片描述

图7-1 二级页表层次结构

 

通过读取多级页表,程序可以通过虚拟内存访问到Hello存放在物理内存中的数据和指令。

7.4 TLB与四级页表支持下的VA到PA的变换

处理器根据TLBI和TLBT寄存器来判断PPN是否已经被缓存到TLB中,如果TLB命中,则直接返回PPN,如果出现缺页,就会到页表中查询PPN。

如果在页表中查询到PPN,则VPN会被分为多段,分别用作各级页表的索引,每个前一级页表的查询结果就是下一级页表的基地址,最后一级页表的查询结果为PPN。PPN和VPO组合得到了PA。整个变换过程如图7-2所示。
在这里插入图片描述

图7-2 虚拟地址变换到物理地址

 

7.5 三级Cache支持下的物理内存访问

在这里插入图片描述

图7-3 物理内存访问

 

7.6 hello进程fork时的内存映射

当shell-bash调用fork函数时,内核为这个用于执行hello的新的进程创建各种数据结构,并分配给它一个唯一的PID。为了该新进程创建虚拟内存,内核创建了shell-bash的mm_struct、区域结构和页表的原样副本。它还会将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在该新进程中返回时(返回0),新进程现在的虚拟内存刚好和调用fork时shell-bash的虚拟内存相同。这两个进程中的任何一个在后面进行写操作时,写时复制机制就会创建新的页面,故用于执行hello的子进程和shell-bash就保持了各自的私有地址空间。

7.7 hello进程execve时的内存映射

shell-bash调用fork函数产生一个子进程,随后在子进程中调用execve函数加载并运行可执行目标文件hello。这个加载并运行的操作会执行以下步骤:

  1. 删除已经存在的用户区域。在这里指shell-bash调用fork函数后生成的子进程。

  2. 映射私有区域。内核为hello的代码、数据、bss和栈区域创建新的区域结构,这些区域都是私有的、写时复制的。

  3. 映射共享区域。比如hello中使用了标准C库libc中的printf函数,此时就需要将该函数映射到hello进程的虚拟地址空间的共享区域内。

  4. 设置程序计数器(PC)。设置hello进程上下文中的程序计数器,使之指向代码区域的入口点

7.8 缺页故障与缺页中断处理

7.8.1 缺页故障

在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页(page
fault),发生缺页时内存和页表的状态如图7-4所示。
在这里插入图片描述

图7-4 缺页异常

 

当触发缺页异常时,缺页异常调用内核中的缺页异常处理程序,在该程序中解决缺页故障,随后程序会返回引起故障的指令(当前指令),重新执行该指令。

7.8.2 缺页中断处理

假设MMU在试图翻译虚拟地址A时,触发了一个缺页。缺页处理程序会执行下面的操作。

  1. 判断虚拟地址A是否合法。搜索区域结构的链表,把虚拟地址A和每个区域结构中的vm_start和vm_end做比较。如果这个指令不合法,那么缺页处理程序会触发一个段错误,终止该进程,如果合法,就继续执行。

  2. 判断内存访问是否合法,即进程是否有读、写或者执行这个区域内页面的权限。如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,终止该进程。

  3. 选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。

所以,当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU,此时,MMU就可以正常地翻译A,而不会再产生缺页中断了。

7.9动态存储分配管理

7.9.1 动态内存管理的基本方法

在C语言程序中,当我们运行时需要额外的虚拟内存时,我们可以使用动态内存分配器实现这一操作。

动态内存分配器维护着一个进程的虚拟内存区域,成为堆(heap),堆紧接在未初始化的数据区域后开始,并向更高的地址生成。分配器将堆视为一组不同大小的块(block)的集合来维护,每个块就是一个连续的虚拟内存片(chunk)。空闲块一直保持空闲,直到它显式地被应用所分配。

在C语言中,可以使用malloc和free函数进行动态内存分配,其函数原型分别为void
*malloc(size_t size)和void free(void
*ptr),malloc函数返回一个指针,指向大小至少为size字节的内存块,如果malloc遇到问题,则返回空指针NULL。free函数用来释放已分配的堆块,ptr参数必须指向一个从malloc、calloc或者realloc获得的已分配块的起始位置,如果不是则会出现未知错误。

7.9.2 动态内存管理的策略

分配器有显式分配器和隐式分配器两种风格。

显式分配器要求应用显式地释放任何已分配的块,比如在C和C++中就必须通过free和delete函数释放动态分配的内存,不然就会发生内存泄漏;隐式分配器会检测每个已分配块何时不再被程序所使用,如果不再使用就释放这个块。隐式分配器也叫做垃圾收集器,自动释放未使用的已分配的块的过程叫做垃圾收集,Lisp、ML以及Java等语言有这样的特性。

分配器会进行合并空闲块的操作,避免可用的空闲块被分割成很多小的、无法使用的空闲块,提高内存的利用率。

分配器可以选择立即合并或者推迟合并,立即合并就是在每次一个块被释放时,就合并所有的相邻块,这个操作可以在常数时间内完成,但是对于某种请求方式,可能会产生抖动;推迟合并就是等到某个稍晚的时候再合并空闲块,例如,分配器可以选择直到某个分配请求失败,然后扫描整个堆,合并所有的空闲块。现在快速的分配器通常会选择某种形式的推迟合并。

7.10本章小结

本章第一节叙述了存储器的地址空间,并在第二节和第三节中分别详细阐述了段式管理和页式管理,第四节分析了带有TLB和4级页表的地址翻译的过程,第五节分析了三级Cache支持下的物理内存访问,第六、七节分别分析了hello
fork和execve时的内存映射,第八节分析了缺页故障和对应的中断处理函数,第九节阐述了动态内存管理的方法和策略。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

在linux下,所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,程序通过Unix
I/O接口的调用实现文件的读写,进而实现对I/O设备的操作

8.2 简述Unix IO接口及其函数

8.2.1 Unix I/O简介

在linux下,所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做相应文件的读和写来执行。这种将设备映射为文件的方式,允许linux内核引出一个简单、低级的应用接口,成为Unix
I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

Unix I/O有open、close、read、write、lseek、stat函数。

8.2.2 open/close函数

open函数用于打开一个已经存在的文件或者创建一个新文件,其函数原型为int
open(char *filename, int flags, mode_t
mode)。open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。

close函数用于关闭一个已经打开的文件,其函数原型为int close(int
fd),关闭一个已经关闭的描述符会出错。

8.2.3 read/write函数

read函数用于在程序中执行输入,其函数原型为ssize_t read(int fd, void
*buf, size_t
n),read函数从描述符为fd的当前文件中复制最多n个字节到内存位置buf。返回值-1表示一个错误,返回值0表示EOF,否则,返回值表示的是实际传送的字节数量。

write函数用于在程序中执行输出,其函数原型为ssize_t write(int fd, const
void *buf, size_t
n),write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。

8.2.4 lseek函数

lseek函数用于使应用程序显示地修改当前文件的位置,其原型为off_t lseek(int
fd, off_t offset, int whence)。

8.2.5 stat函数

stat函数用于检索关于文件的信息(元数据),其原型为int stat(const char
*filename, struct stat
*buf),stat函数以一个文件名作为输入,并填写stat数据结构中的各个成员,包括文件的字节数大小、inode号、访问时间、数据修改时间、状态修改时间等。

8.3 printf的实现分析

printf函数的实现如图8-1所示,我们可以看到,在printf函数中调用了vsprintf函数和write函数。
在这里插入图片描述

图8-1 printf函数实现

 

vsprintf函数的作用是格式化,它接收确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出,并传递给write函数。
在这里插入图片描述

图8-2 write函数实现

 

write函数的实现如图8-2所示,write函数调用硬件将数据输出到终端。在write函数中进行了系统调用,即int
INT_VECTOR_SYS_CALL,该系统调用的功能是通过直接写显存来在终端中显式字符串。

8.4 getchar的实现分析

当用户按下键盘时,触发键盘中断处理子程序,该程序将按键扫描码转换成ASCII码并保存到标准输入缓冲区。

一种getchar函数的实现如图8-3所示,getchar通过调用read函数从缓冲区中得到一个字符。
在这里插入图片描述

图8-3 getchar函数实现

 

8.5本章小结

本章第一节阐述了linux下IO设备的管理方式,第二节分析了Unix
I/O接口及其包含的函数,第三、四节分别研究了printf和getchar函数的具体实现。

结论

程序员通过键盘将程序输入进文本编辑器,生成hello.c源文件;预处理器对hello.c进行预处理,生成hello.i;编译器将hello.i编译生成汇编语言文件hello.s;汇编器将hello.s汇编生成可重定位目标文件hello.o;最后由连接器将hello.o和其他库函数链接生成可执行文件hello。

程序员在操作系统提供的shell程序中输入./hello命令,shell调用fork函数创建子进程,在子进程中调用execve函数将hello复制到内存中并运行。操作系统为hello分配时间片,在一个时间片中,hello享有CPU资源,执行程序中的指令。

hello调用了printf函数,该函数是标准输出函数,通过调用Unix
I/O进行屏幕输出。printf函数调用了malloc函数,通过显式分配器进行动态内存申请。

hello在执行的过程中,需要访问各种地址,该地址为虚拟地址,MMU将虚拟地址映射为物理地址,用于实际访问内存芯片。

同时,程序运行的过程中还可能接收到各种IO设备输入的信号,hello可以通过异常控制流对信号进行处理,也可以处理运行过程中产生的故障和错误。

hello程序结束后,shell父进程负责该子进程的回收,内核删除为hello创建的所有数据结构。

通过这一学期计算机系统的学习,我对计算机有了一个整体的认识。在学习的所有内容中,我对hello的预处理-编译-汇编-链接这整个过程和进程管理非常感兴趣,前者提高了我编写程序的能力,后者使我解开了很久以来的疑惑。我认为这可能就是CSAPP和计算机系统这门课的魅力所在:让我们对计算机所有内容有一个大概的了解,并从中发现自己真正感兴趣的内容。

附件

文件名称 文件描述


hello.c 老师提供的C语言源代码
hello.i hello.c预处理后生成的文件
hello.s hello.i编译后生成的汇编语言文件
hello.o hello.s汇编后生成的可重定位目标文件
hello hello.o链接后生成的可执行目标文件
hello_o_elf.txt hello.o文件的ELF格式
hello_elf.txt hello文件的ELF格式
hello_o_disassembly.s hello.o反汇编得到的文件
hello_disassembly.s hello反汇编得到的文件

参考文献

[1] 兰德尔E.布莱恩特,大卫R.奥哈拉伦.
深入理解计算机系统(第3版).机械工业出版社. 2018.4.

[2] 沈超,李明.
细说Linux基础知识(第二版).北京:电子工业出版社,2019.10.

[3] Brian W. Kernighan, Dennis M. Ritchie.
C程序设计语言(第2版·新版).北京:机械工业出版社. 2004.1.

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值