哈工大计算机系统大作业: 程序人生-Hello’s P2P/ hello 的一生
题 目 程序人生-Hello’s P2P
专 业 计算机
学 号 120L020624
班 级 2003010
学 生 孙楚芮
指 导 教 师 郑贵滨
计算机科学与技术学院
2022年5月
摘 要
本文主要讲述了hello.c程序在编写完成后运行在linux中的生命历程,借助相关工具分析预处理、编译、汇编、链接等各个过程在linux下实现的原理,分析了这些过程中产生的文件的相应信息和作用。
关键词:
预处理;编译;汇编;链接;shell;虚拟内存;IO;存储;
Hello的一生
第一章 概述
1.1 Hello简介
1) From Program to Process
首先我们需要编程,即program编写程序hello.c,然后这个文件会进行一系列的预处理,编译,汇编,链接生成一个.out可执行文件,我们打开shell之后,如果去运行它,那么hello就会为hello创建一个新的进程,即progress,然后调用execve函数,将hello的可执行文件进行加载,运行。
2) FROM ZERO TO ZERO
首先在真正加载前shell这个壳需要为hello申请虚拟内存空间,然后将物理内存与虚拟内存进行映射,同时处理器内核还需要为hello分配时间片,使得其看似独享整个资源,当需要物理地址时,cpu会向mmu发出一个虚拟地址,mmu将虚拟地址转化为物理地址之后,在提供相应的数据。同时当我们在程序运行期间,输入信号时,会使内核产生一个中断,当按下ctrl-z 或 ctrl-c时,会向hello进程发送一个信号,如果子进程终止之后,会有父进程将僵尸进程进行回收,避免占用资源。
1.2 环境与工具
硬件: Core I7-10875H X64 ; 32G RAM; 1TB SSD
软件: Ubuntu 20.10
工具: VIM , GCC, EDB, OBJDUMP, READELF, LD
1.3 中间结果
hello.c:源 程序
hello.i: 经过预处理的源程序
hello.s:hello.i 经过编译的汇编程序
hello.o:hello.s 经过汇编的可重定位目标程序
hello:hello.o 经过链接后的可执行目标程序
hello.elf:hello.o 的 elf 格式
hello.elf:hello 的 elf 格式
helloo.objdump:hello.o的反汇编程序
hello.objdump:hello的反汇编程序
1.4 本章小结
本章节简述了P2P 和 020 的含义,列出了测试环境和工具和中间结果的文件名和文件作用。从整体上大致介绍了hello的一生,并且列出了做本次作业的软硬件环境以及工具。也相当于漫游了一下hello的一生。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:
预处理器根据以字符#开头的命令修改原始的C程序。 比如 hello.c 中的命
令告诉预处理器读取对应的三个系统头文件的内容,并把它直接插入到程
序文本中,结果就得到了另一个C程序。其中,ISO C/C++要求支持的包括#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。
预处理的作用:
1.#include指令告诉预处理器(cpp)读取源程序所引用的系统源文件,并把源文件直接插入程序文本中。
2.执行宏替换。将目标的字符替换为我们所定义的字符。
3.条件编译。根据定义的条件,来确定编译的条件,即目标是否是真正需要的,类似于ifelse。
4.特殊符号,预编译程序可以识别一些特殊的符号,预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
首先使用vim打开hello.i
就像这样
然后找到main函数
在这之前出现的是头文件 stdio.h、unistd.h、stdlib.h
的依次展开。
以 stdio.h 的展开为例:stdio.h是标准库文件,cpp 到Ubuntu中默认的环
境变量下寻找stdio.h,打开文件/usr/include/stdio.h ,发现其中依然使用
了#define语句,cpp对stdio中的define宏定义递归展开。
最终.i文件将预处理指令进行替换,而原来的预处理指令就不需要了;发现其中使用了大量的#ifdef#ifndef条件编译的语句,cpp会对条件值进行判断来决定是否执行包含其中的逻辑。特殊符号,预编译程序可以识别一些特殊的符号,预编译程序对于在源程序中出现的这些串将用合适的值进行替换。在下面我们可以看到其寻找的头文件。
2.4 本章小结
本章主要介绍了预处理的定义与作用,并且分析了.i文件的整个分析过程、并结合.i程序对预处理结果进行了分析。
第3章 编译
3.1 编译的概念与作用
编译的概念:
编译是利用编译程序从预处理文本文件产生汇编程序的过程。主要包含五个阶段:词法分析;语法分析;语义检查、中间代码生成、 目标代码生成。
编译的作用:
词法分析:
将源代码程序输入扫描器,将源代码的字符序列分割成一系列记号。
语法分析:
基于词法分析得到的一系列记号,生成语法树。
语义分析:
由语义分析器完成,指示判断是否合法,并不判断对错。
目标代码的生成与优化:
目标代码生成阶段编译器会选择合适的寻址方式,左移右移代替乘除,删除多余指令。
3.2 在Ubuntu下编译的命令
然后我们用vim打开
3.3 Hello的编译结果解析
hello.c的数据类型有整型、字符串、数组
整数
.data节是8字节对齐的
hello.c中还有argc和i两个整型变量。其中int i是循环中用来计数的局部变量,argc是从终端输入的参数的个数,也是main函数的第一个参数。hello.s将i存储在-4(%rbp)中,初始值为0,每次循环加1,退出循环条件是i大于7。
字符串
编译器一般会将字符串存放在.rodata节。hello.c中共有两个字符串,分别是两个printf格式化输出的字符串。对应找到hello.s中的.rodata节:
可以看出 hello.s 中字符串由.string 声明,第一个字符串.LC0包含汉字,每个汉字在utf-8编码中被编码为三个字节;第二个字符串的两个*%s*为用户在终端输入的两个参数。
数组
hello.c中的数组是 main()函数的第二个参数 char *argv[],argv是字符串指针的数组,每个元素是一个指向一个字符串首地址的指针,作为函数的第二个参数 ,argv[]开始被保存在寄存器%rsi中,然后又被保存到栈中32 (%rbp)的位置。
可以看到 main 函数并没有访问 argv【0】,而是访问了 argv【1】和 argv【2】,这是因为argv[0]指向程序运行的全路径名。
函数操作
程序运行时先进入程序入口处,然后自动调用 main函数。若程序员需要调用 函数,在汇编代码中需要使用 call指令,在使用之前需要先设置好参数(%eax)。进行传参。
结束后的ret: %rbp为栈帧的底部,函数在%rbp上分配空间
leave指令:相当于mov %rbp,%rsp ,pop %rbp,恢复栈空间为调用main函 数之前的状态。
3.4 本章小结
本节对应于书上与汇编语言相关的章节,总结并分析了编译器是如何处理c语言的各个数据类型和各类操作,如算术操作,关系操作和函数调用的。经过该步骤hello.s已经是更加接近机器层面的汇编代码。
第4章 汇编
4.1 汇编的概念与作用
- 汇编的概念:汇编器(as)将hello.s翻译成机器能读懂的机器语言指令,并将这些指令打包成可重定位目标程序hello.o,hello.o是一个二进制文件
- 作用:产生机器能读懂的代码,使得程序能被机器执行。由于几乎每一条汇编指令都对应于一 条机器代码。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
命令为: gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
输入: *readelf -a hello.o > hello.elf *指令.获得ELF格式文件。
- ELF Header:以16B的序列Magic开始,Magic描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。
- Section Headers:节头部表,包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。
- 重定位节.rela.text,一个.text节中位置的列表,包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
8条重定位信息分别是对.L0(第一个printf中的字符串)、puts函数、exit函数、.L1(第二个printf中的字符串)、printf、sleepsec、sleep、getchar函数进行重定位声明。
以.L1的重定位为例阐述之后的重定位过程:链接器根据info信息向.symtab 节中查询链接目标的符号,由 info.symbol=0x05,可以发现重定位目标链接到.rodata 的.L1,设重定位条目为r,r的构造为:r.offset=0x18, r.symbol=.rodata, r.type=R_X86_64_PC32,
r.addend=-4重定位一个使用 32 位 PC相对地址的引用。计算重定位目标地址的算法如下(设需要重定位的.text节中的位置为 src,设重定位的目的位置 dst):
refptr = s +r.offset (1)
refaddr = ADDR(s) + r.offset (2)
*refptr = (unsigned) (ADDR(r.symbol) + r.addend-refaddr)(3)
其中(1)指向 src 的指针(2)计算src的运行时地址,(3)中ADDR (r.symbol)计算dst的运行时地址,在本例中,ADDR(r.symbol)获得的是 dst的运行时地址,因为需要设置的是绝对地址,即 dst与下一条指令之间的地址之差,所以需要加上 r.addend=-4。之后将 src处设置为运行时值为*refptr,完成该处重定位。
- 符号表(SymbolTable)目标文件的符号表中包含用来定位、重定位程序中符号定义和引用的信息。符号表索引是对此数组的索引。索引0表示表中的第一表项,同时也作为定义符号的索引。
4.4 Hello.o的结果解析
objdump -d -r hello.o分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
使用 objdump -d -r hello.o > helloo.objdump获得反汇编代码。Hello.s和helloo.objdunp除去显示格式之外两者差别不大,主要差别如下:
分支转移:反汇编代码跳转指令的操作数使用的不是段名称如.L3,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
函数调用:在.s文件中,函数调用之后直接跟着函数名称,而在反汇编序中,call的目标地址是当前下一条指令。这是因为hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call 指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text节中为其添加重定位条目,等待静态链接的进一步确定。
全局变量访问:在.s 文件中,访问 rodata(printf中的字符串),使用段名称+%rip,在反汇编代码中 0+%rip,因为 rodata中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.5 本章小结
本章介绍了 hello 从 hello.s 到 hello.o 的汇编过程,通过查看 hello.o 的elf格式和使用objdump得到反汇编代码与hello.s进行比较的方式,间接了解到从汇编语言映射到机器语言汇编器需要实现的转换
第5章 链接
5.1 链接的概念与作用
概念:
是将各种代码和数据片段收集并组合为单一文件的过程,这个文件可以被加载(复制)到内存并执行。
作用:
-
链接可以执行于编译时,也就是源代码被翻译成机器代码时;也可以执行于加载时,即程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。
-
链接使得分离编译(seperate compila)成为可能。更便于我们维护管理,我们可以独立的修改和编译我们需要修改的小的模块。
注意:这儿的链接是指从 hello.o 到 hello 生成过程。
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的格式
- 各节的基本信息均在节头表(描述目标文件的节)中进行了声明。节头表(包括名称,大小,类型,全体大小,地址,旗标,偏移量,对齐等信息),下面是它的截图。
5.4 hello的虚拟地址空间
分析程序头LOAD可加载的程序段的地址为0x400000。
在0x401000~0x402000段中,程序被载入,虚拟地址0x401000开始,到0x401ff0结束,根据5.3中的节头部表,可以通过edb找到各个节的信息,比如.txt节,虚拟地址开始于0x4010f0,大小为0x145.
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
命令: objdump -d -r hello > hello.txt
hello和hello.o相比,首先多了很多经过重定位之后的函数,如_init、puts@plt等,hello.o在.text段之后只有一个main函数;hello.o的地址是从0开始的,是相对地址,而hello的地址是从0x401000(_init的地址)开始的,是已经进行重定位之后的虚拟地址;在hello的main函数中,条件跳转指令和call指令后均为绝对地址,而hello.o中是相对于main函数的相对地址。
链接器完成的两个主要任务:符号解析和重定位。
重定位由两步组成:
- 重定位节和符号定义。
- 重定位节中的符号引用。
如对puts函数的重定位:
在hello.o反汇编代码中,该行二进制编码为e8 00 00 00 00
addr(text)= 0x401105
refaddr = addr(text)+offset = 0x401126,即引用运行时的地址
addr(r.symbol) = addr(puts) = 0x401080
然后更新该引用,*refptr = (unsigned) (addr(r.symbol) + r.addend - refaddr)
= (unsigned) (0x401080 +(-4) – 0x401126) = (unsigned) (-aa) = ff ff ff 56
将其以小段序填入可得 56 ff ff ff ,与反汇编代码一致。
5.6 hello的执行流程
使用EDB,只需要逐步执行,将程序名列出即可:
函数 | 地址 |
---|---|
ld-2.27.so!_dl_start | 0x7fce 8cc38ea0 |
ld-2.27.so!_dl_init | 0x7fce 8cc47630 |
hello!_start | 0x400500 |
libc-2.27.so!__libc_start_main | 0x7fce 8c867ab0 |
-libc-2.27.so!__cxa_atexit | 0x7fce 8c889430 |
-libc-2.27.so!__libc_csu_init | 0x4005c0 |
hello!_init | 0x400488 |
libc-2.27.so!_setjmp | 0x7fce 8c884c10 |
-libc-2.27.so!_sigsetjmp | 0x7fce 8c884b70 |
–libc-2.27.so!__sigjmp_save | 0x7fce 8c884bd0 |
hello!main | 0x400532 |
hello!puts@plt | 0x4004b0 |
hello!exit@plt | 0x4004e0 |
*hello!printf@plt | – |
*hello!sleep@plt | – |
*hello!getchar@plt | – |
ld-2.27.so!_dl_runtime_resolve_xsave | 0x7fce 8cc4e680 |
-ld-2.27.so!_dl_fixup | 0x7fce 8cc46df0 |
–ld-2.27.so!_dl_lookup_symbol_x | 0x7fce 8cc420b0 |
libc-2.27.so!exit | 0x7fce 8c889128 |
5.7 Hello的动态链接分析
dl_init 调用后,0x601008 和 0x601010 两个地址 的数 据 都 产 生 变 化。
由于编译器无法预测函数的运行时地址,所以需要添加重定位记录。链接器
采用延迟绑定的策略,使用 PLT+GOT实现函数的动态链接。
PLT 使用 GOT 中的地址跳到目标函数。
在 dl_init 调用之前,函数调用都指向 PLT 中的代码逻辑。第一次执行时,为 GOT赋上相应的偏移量,初始化了函数调用,dl_init就是做了这件事。此后每次执行时不需要经过如此操作,每次都直接跳转到目标函数的地址。
5.8 本章小结
在本章中主要介绍了链接的概念与作用、hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。
链接器的两个主要任务就是符号解析和重定位,符号解析将目标文件中的每个全局符号都绑定到一个唯一的定义,而重定位确定每个信号的最终内存地址,并且修改对那些目标符号的引用。
第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程是程序的一个实例,每一个进程都有它自己的地址空间。在地址空间内,每个进程的地址空间结构的一样的。
作用:
进程为用户提供了如下假象:程序好像在独占处理器、内存,处理器无间断 地运行进程,该进程好像是系统中唯一运行的程序。
6.2 简述壳Shell-bash的作用与处理流程
- Shell 是一个程序,它可以读取用户输入的命令,执行相应的操作。
- Shell 应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
- 处理流程: 当从 shell里输入命令(字符串)时,第一个单词是可执行程序的名称,后面则是参数列表。
- shell 会传进参数列表来执行对应程序,创建进程,并在进程终止后回收进程。读入后,shell 先解析字符串,得到命令行参数(char
**argv)。 - 若命令行参数 的最后一个单词是&,表示要在后台执行,shell可以继续输入命令来做其他工作, 否则则为前台执行,必须等待该进程结束并回收。
6.3 Hello的fork进程创建过程
在终端Terminal中键入./hello 120L020624 孙楚芮,运行的终端程序会对输入的命令行进行解析。
- hello 不是一个内置的shell命令所以解析之后终端程序判断./hello
的语义为执行当前目录下的可执行目标文件 hello - 之后终端程序首先会调用 fork函数创建一个新的运行的子进程,新创建的子进程几乎父进程相同,但不完全与相同。fork函数的返回值里,一般用0代表子进程,非0代表父进程
- 父进程与子进程之间最大的区别在于它们拥有不同的 PID。子进程得到与父进程用户级虚拟地址空间相同的一份副本,当父进程调用 fork时,子进程可以读写父进程中打开的任何文件。
- 内核能够以任意方式交替执行父子进程的逻辑控制流的指令,父进程与子进程是并发运行而独立的。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
- 父进程和子进程独立 运行,二者结束顺序不可知。父进程负责回收子进程。 所以可以根据 fork 的返回值不同来区分子进程和父进程。
6.4 Hello的execve过程
- 进程调用 execve 函数,该函数从不返回,它将删除该进程的代码和地址空间内的内容并将其初始化。
- 加载目标程序。换句话说,execve 函数将用目 标 程 序 的 进 程 替 换 当前进程,并传入相应的参数和环境变量,控制转移到新程序的main函数。
6.5 Hello的进程执行
- 由于运行 hello 进程的同时可能在同时运行其他进程,内核并发运行多个进程需要用到上下文切换来实现多任务。内存为每个进程维持一个上下文,即内核重新启动一个被抢占的进程所需的状态。假如 hello 没有被抢占,则一条条运行汇编指令;若被抢占,内核则进行上下文切换:假如从 hello 进程切换到 A 进程,则
-
保存 hello 进程的上下文
-
恢复 A 进程的上下文
-
控制转移给 A 进程
-
hello 进程最初运行在用户模式,但是程序调用了 sleep 函数,调用时产生了用户态和核心态的转变。进程主动请求休眠,于是产生上述的上下文切换,内核将控制转移到其他进程,将 hello 进程从运行队列加入等待队列,从用户模式变成内核模式,并开始计时。当计时结束时,发送中断信号,将 hello 进程从等待队列中移出,从内核模式转为用户模式。此时 hello 进程就可以继续执行逻辑控制流了。
-
在getchar 时,实际上也是执行了 read 的调用,此时也产生了如上所述的上下文切换,进程等待键盘缓冲区的输入。当完成键盘缓冲区到内存的数据传输后,内核从其他进程上下文切换到hello 进程。
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.6 hello的异常与信号处理
- ctrl + z 向进程发送信号,使进程挂起。
- ctrl + c 向进程发送信号, 令进程终止:
- ps fg 将进程放置后台(前台)运行:
当前进程:
kill之后的进程:
- 在进行乱按的时候,第一个字符串会被读入,而其他的字符串会在缓冲区中储存,然后再程序结束的时候,当成命令行命令读入.
6.7本章小结
异常控制流发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制。有四种不同类型的异常,中断,故障,终止,和陷阱。
在操作系统层,内核用ECF提供进程的基本概念,给应用程序两个重要的抽象:
- 逻辑控制流
- 私有地址空间.
应用程序可以创建子进程,等待他们的子进程停止或者终止,运行新的程序,以及捕获来自其他进程的信号.
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:
程序代码经过编译后出现在汇编程序中地址。
线性地址:
逻辑地址经过段机制转化后为线性地址,用于描述程序分页信息的地址。以 hello 为例,线性地址就是 hello 应该在内存的哪些块上运行。
虚拟地址:
同线性地址。此外,有些资料是直接把逻辑地址当成虚拟地址,两者并没有明确的界限。
物理地址:
处理器通过地址总线的寻址,找到真实的物理内存对应地址。是内存单元的真实地址。以 hello 为例,物理地址就是 hello 真正应该在内存的哪些地址上运行。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理,是指把一个程序分成若干个段进行存储,每个段都是一个逻辑实 体,程序员需要知道并使用它。
它的产生是与程序的模块化直接有关的。段式管 理是通过段表进行的。它包括段号或段名、段起点、装入位、段的长度等。此外还需要主存占用区域表、主存可用区域表。
7.3 Hello的线性地址到物理地址的变换-页式管理
首先查看页表
然后使用页表的地址翻译:
线性地址转换成物理地址的过程如下:
- 首先我们先将线性地址分为 VPN(虚拟页号)+VPO(虚拟页偏移)的形式。
- 然后再将 VPN 拆分成 TLBT(TLB 标记)+TLBI(TLB 索引)然后去 TLB 缓存里 找所对应的 PPN(物理页号)如果发生缺页情况则直接查找对应的PPN,找到 PPN 之后,将其与 VPO 组合变为 PPN+VPO 就是生成的物理地址了。
7.4 TLB与四级页表支持下的VA到PA的变换
书中的二级页表示例:
在实际的运行的过程中,二级页表是远远不够的,因此我们需要多级页表:
在多级页表中,每一级页表中的一个项都对应下一页表的起始位置,这样不需要的页表就不需要载入内存中,节省空间。
7.5 三级Cache支持下的物理内存访问
此时我们已经得到物理地址,只需要在 cache 寻找即可。与课本高速缓存章节 类似,将物理地址分为 CT(标记)+CI(索引)+CO(偏移量),然后在一级 cache 内部找,如果没有一直向下递归寻找。找到之后将其写入cache, 返回结果。
- 我们先在L1缓存中寻找结果,如果命中,就将缓存中的数据项保存,返回递归向下一级存储结构中寻找。
7.6 hello进程fork时的内存映射
当 fork 函数被 shell进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello 中的程序
-
删除已存在的用户区域
-
映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构。
-
映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
-
设置程序计数器(PC),使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
假设当前虚拟地址是 A,现在翻译地址 A 触发了一个缺页异常,导致控制转
移到内核的缺页处理程序,处理程序将执行以下步骤:
-
判断A 在某个区域结构定义的区域是否合法,若不合法,则产生一个段错误,然后终止这个进程。
-
判断该内存访问是否合法,若访问是不合法的,那么缺页处理程序会触发一个保护异常,终止这个进程。
-
如果此时内核知道该操作是合法的,那么将把对应页面加载并更新页表,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。
-
当缺页处理程序返回时,CPU 重新启动引起缺页的指令。即可。
7.9动态存储分配管理
printf 函数会调用 malloc,下面简述动态内存管理的基本方法与策略:
-
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。
-
每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。
-
空闲块可用来分配。空闲块保 持 空 闲,直到它显式地被应用所分配。
-
一个已分配的块保持已分配状态,直到它被释放。
带边界标签的隐式空闲链表
- 堆及堆中内存块的组织结构:
在内存块中增加4B的Header(用于寻找下一个blcok)和4B的Footer(用于寻找上一个block)。Footer的设计是专门为了合并空闲块方便的。因为Header和Footer大小已知。
- 隐式链表
对比于显式空闲链表,隐式空闲链表代表并不直接对空闲块进行链接,而是将对内存空间中的所有块组织成一个大链表。
- 空闲块合并
可以利用Footer方便的对前面的空闲块进行合并。合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。通过改变Header和Footer四种情况分别进行空闲块合并。
显示空间链表
将空闲块组织成链表形式的数据结构。堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和 succ(后继)指针。
7.10本章小结
虚拟内存是对主存的一个抽象,它提供三个重要的功能,第一,自动缓存最近使用的存放磁盘上的虚拟地址空间。第二,简化内存管理,进而简化链接,在进程间共享数据,以及程序的加载。
内存的使用和释放是一个容易出错的地方,需要我们进一步理解对它的认识,也要在编程工作中,优化内存管理的策略。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。
这就使得所有的输入和输出都能一一个统一且一致的方式来执行:
-
打开文件。
-
Linux Shell创建的每个进程开始时都有三个打开的文件:标准输入,标准输出,标准错误。
-
改变当前文件的位置。
-
读写文件。
-
关闭文件。
8.2 简述Unix IO接口及其函数
接口:
-
打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符。描述符在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
-
Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
-
改变当前的文件位置:内核保持着每个打开的文件的一个文件位置k。k初
始为0。这个文件位置k表示的是从文件开头起始的字节偏移量。 -
读写文件
读操作就是从文件复制n>0个字节到内存。
写操作就是从内存中复制n>0个字节到一个文件。 -
关闭文件:内核释放文件打开时创建的数据结构,无论一个进程以何种原因终止时,内核都会关闭所有打开的文件,并且释放他们的内存资源。
函数:
int open(cahr filename, int flags, mode_t mode);
int close (int fd);
ssize_t read (int fd, void buf, size_t n);
ssize_t write (int fd, const buf, size_t n);
8.3 printf的实现分析
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用int
0x80或syscall等.字符显示驱动子程序:从ASCII到字模库到显示vram。显示
芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码
转成ascii码,保存到系统的键盘缓冲区。
getchar函数在实现时到底层调用了系统函数read,通过系统调用read读取存储在键盘缓冲区中的ASCII码直到读到回车符然后返回整个字串,getchar进行封装,大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
Linux 提供了少量的基于 Unix i/o模型的系统函数,他们允许应用程序打开,关闭,读写文件,执行io重定向。
而标准io库是基于unix io 实现的,并且提供了一组强大的高级io 例程,对于大多数应用程序而言,标准io更加简单,是优于unix io的选择,但 是对标准io和网络文件的一些互相不兼容的显式,unix io比标准io更适合用于网络应用程序的编程。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
预处理:
hello.c预处理到hello.i文本文件;
编译:
hello.i编译到hello.s汇编文件;
汇编:
hello.s汇编到二进制可重定位目标文件hello.o;
链接:
hello.o链接生成可执行文件hello;
创建子进程:
bash进程调用fork函数,生成子进程;
加载程序:
execve函数加载运行当前进程的上下文中加载并运行新程序hello;
访问内存:
hello的运行需要地址的概念,虚拟地址是计算机系统最伟大的抽象;
交互:
hello的输入输出与外界交互,与linux I/O息息相关;
终止:
hello最终被shell父进程回收,内核会收回为其创建的所有信息。
至此,hello运行结束。
附件
hello | hello的执行程序 |
---|---|
hello.c | hello 的源程序 |
hello.elf | hello 的elf头部 |
hello.i | hello 的预处理程序 |
hello.o | hello 的可重定位的目标程序 |
hello.txt | hello 的反汇编文件 |
hello.s | hello 的汇编程序 |
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 深入理解计算机系统第三版
[2] 百度,知乎,掘金,CSDN。
[3] bilibili cmu网课
[4] gcc–编译的四大过程及作用:https://blog.csdn.net/shiyongraow/article/details/81454995
[5] 网络用户. 阿里云. ELF格式文件符号表全解析及readelf命令使用方法. 2018:07-19.
https://www.aliyun.com/zixun/wenji/1246586.html
[6] C语言预处理命令之条件编译. 2009:08-16.
http://www.kuqin.com/language/20090806/66164.html
[7] CSDN. 编译器工作流程详解. 2014:04-27.
https://blog.csdn.net/u012491514/article/details/24590467