哈尔滨工业大学2022春季学期CSAPP大作业

此大作业涵盖CSAPP一大本书的内容,可以很好地帮助复习和理解其中的内容。

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算学部
学   号 120L022308
班   级 2003008
学 生 燕道华    
指 导 教 师 吴锐

计算机科学与技术学院
2022年5月

摘 要

本文通过运用Ubuntu对hello程序在Linux下的生命周期进行分解,用CSAPP课上所学进行完整细致的分析,目的是对计算机系统有更深的理解。

关键词:hello;计算机系统;Ubuntu;程序的生命周期

目 录

第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:程序员通过编辑器创建并保存文件名为hello.c的文本文件,经过cpp的预处理,ccl的编译,as的汇编和ld的链接成为可执行目标文件,在shell中键入启动命令后,shell为其fork产生一个子进程,然后hello便从程序变为了进程。

020:shell为此子进程execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。

1.2 环境与工具
硬件:
处理器:AMD Ryzen 5 3600 6-Core Processor 3.59GHz;
已安装的内存(RAM):16G(15.9G可用);
系统类型:64位操作系统,基于x64的处理器;
软件:Linux: Vmware虚拟机; Ubuntu 18.04 LTS 64位/优麒麟 64位;
开发与调试工具:gcc,as,ld,vim,edb,readelf,VScode。

1.3 中间结果
文件名 文件作用
hello.i hello.c预处理得到的文本文件
hello.s hello.i编译而成的汇编程序
hello.o hello.s汇编而成的可重定位目标程序
hello hello.o链接成可执行目标文件
hello.out hello反汇编而成的反汇编文件
elf.txt hello.o的ELF 格式
hello1.elf hello的ELF 格式

1.4 本章小结
本章对hello程序进行了概括,介绍了P2P,020过程,列出了本次作业的硬件软件环境及开发工具和从hello.c到hello的中间文件。

第2章 预处理

2.1 预处理的概念与作用
概念:预处理中会展开以#起始的行,试图解释为预处理指令(preprocessing directive) ,其中ISO C/C++要求支持的包括#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令) 。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。
作用:
1.将源文件中以”include”格式包含的文件复制到编译的源文件中。
2.用实际值替换用“#define”定义的字符串。
3.根据“#if”后面的条件决定需要编译的代码。

2.2在Ubuntu下预处理的命令
在这里插入图片描述
图一:输入预处理命令

输入命令:gcc -E hello.c -o hello.i。

2.3 Hello的预处理结果解析
可以发现,hello.i相较hello.c已经扩展了三千余行,原来的代码出现在hello.i的最后。在源代码之前是头文件stdio.h unistd.h stdlib.h的展开,头文件中的内容被包含进该文件中,且对 stdio 中的define 宏定义递归展开,此外,预编译程序可识别一些特殊的符号,预编译程序对在源程序中出现的这些串将用合适的值进行替换。
在这里插入图片描述
图二:头文件的宏展开
在这里插入图片描述
图三:hello.i中的源代码

2.4 本章小结
本章主要介绍了预处理(包括头文件的展开、宏替换、去掉注释、条件编译)的概念和功能,同时在Ubuntu上输入命令进行预处理并作结果解析。

第3章 编译

3.1 编译的概念与作用
概念:编译过程是整个程序构建的核心部分,编译成功,会将源代码由文本形式转换成机器语言,编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。编译器将文本文件 hello.i 翻译成汇编程序hello.s。
作用:
把源程序(高级语言)翻译成汇编语言或机器语言。其包括以下几个步骤:
1、词法分析:词法分析是使用一种叫做lex的程序实现词法扫描,它会按照用户之前描述好的词法规则将输入的字符串分割成一个个记号。产生的记号一般分为:关键字、标识符、字面量(包含数字、字符串等)和特殊符号(运算符、等号等),然后他们放到对应的表中。
2、语法分析:语法分析器根据用户给定的语法规则,将词法分析产生的记号序列进行解析,然后将它们构成一棵语法树。对于不同的语言,只是其语法规则不一样。
3、中间代码:源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。
4、目标代码生成与优化:代码生成器将中间代码转成机器代码,这个过程是依赖于目标机器的,因为不同的机器有着不同的字长、寄存器、数据类型等。最后目标代码优化器对目标代码进行优化,比如选择合适的寻址方式、使用唯一来代替乘除法、删除出多余的指令等。

3.2 在Ubuntu下编译的命令
在这里插入图片描述
图四:输入编译命令

输入命令:gcc -S hello.c -o hello.s。

3.3 Hello的编译结果解析
3.3.1汇编指令的介绍
在这里插入图片描述
图五:汇编指令

.file:声明源文件
.text:代码节
.section:
.rodata:只读代码段
.align:数据或者指令的地址对齐方式
.string:声明一个字符串(.LC0,.LC1)
.global:声明全局变量(main)
.type:声明一个符号是数据类型还是函数类型

3.3.2数据
1、常量
在语句
在这里插入图片描述
图六:常量4的出现

中,常量4被储存在寄存器%rbq中,也就是.text中
在这里插入图片描述
图七:常量4的引用

同理,在循环语句
在这里插入图片描述
图八:循环语句中的常量1、2、3

中,常数1、2、3也是被存储在.text中,作为指令的一部分。
2、字符串
此hello.s文件中的字符串在图五中已经出现过了,这两个字符串作为printf函数的参数,保存在只读数据段中。
3、变量
全局变量:储存在.data或.bss节中。初始化的全局变量储存在.data节,它的初始化不需要汇编语句,而是直接完成的。
局部变量:在main函数中定义了一个局部变量i。
在这里插入图片描述
图九:i的赋值

编译器进行编译的时候会将局部变量i放在堆栈中。如图所示,局部变量i放在栈上-4(%rbp)的位置。
3.3.3赋值
如图九,将局部变量i用movl赋初值为0。
3.3.4算术操作
在图八中,我们可以看到i进行了i++自加操作。
在这里插入图片描述
图十:i++操作

在每次循环执行的内容结束后,用addl对i进行一次自加,栈上存储变量i的值加1。
3.3.5关系操作
在图六和图八中,分别出现了if(argc!=4)和for(i=0;i<8;i++)两处关系操作,前者通过图七中的cmpl指令设置的条件码来判断是否需要跳转到分支中,后者通过如下图所示的cmpl指令设置的条件码(即如果小于等于7就跳转)来判断是否跳转到指定地址。
在这里插入图片描述
图十一:for语句中的关系操作

3.3.6数组/指针/结构操作
主函数main的参数中有指针数组char *argv[],数组的每个元素都是一个指向字符类型的指针。
在这里插入图片描述
图十二:main函数定义
在这里插入图片描述
图十三:argc和argv的存储位置
在这里插入图片描述
图十四:对argv数组成员的取值

以上三图说明argc存储在%edi中,argv存储在%esi中,图十四用movq指令获取argv[1]和argv[2]的地址。
3.3.7控制转移
汇编语言中首先设置条件码,然后根据条件码来进行控制转移。在hello.c中出现了三次次控制转移。第一次如图七所示:如果参数等于4,则控制转移,跳转到L2。第二次如图九所示:给i赋完初值后直接跳转到L3。第三次如图十一所示:如果i小于等于7则跳转到L4。
3.3.8函数操作
X86-64中,过程调用传递参数规则:第1~6个参数一次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,剩下的参数保存在栈当中。
main函数:
参数传递:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储。
局部变量:创建了一个int类型的局部变量i。
函数调用:被系统启动函数调用。
函数返回:设置%eax为0并且返回,对应return 0 。
(参考3.3.6节中的图)

printf函数:
参数传递:call puts时只传入了字符串参数首地址;for循环中call printf时传入了 argv[1]和argc[2]的地址。
函数调用:if判断满足条件后调用,与for循环中被调用。
(参考3.3.6节中的图)

exit函数:
参数传递:传入的参数为1,再执行退出命令。
函数调用:if判断条件满足后被调用。
(函数出现可参考图十三)

sleep函数:
参数传递:传入参数atoi(argv[3])。
函数调用:for循环下被调用,call sleep。
(函数出现可参考图十四)

getchar函数:
函数调用:在main中被调用,call getchar。
在这里插入图片描述
图十五:getchar函数的调用

3.3.9类型转换
hello.c中涉及的类型转换是:atoi(argv[3]),将字符串类型转换为整数类型(如图十四)。

3.4 本章小结
本章主要讲述了编译的概念与作用,编译阶段中编译器如何处理各种数据和操作,以及C语言中各种类型和操作所对应的的汇编代码。通过从C语言的数据与操作的不同方面进行详细的探究,加深了我对汇编的理解。

第4章 汇编

4.1 汇编的概念与作用
概念:汇编是指把汇编语言翻译成与之等价的机器语言。汇编输入的是汇编语言,输出的是机器语言。此机器语言是可重定位目标程序。
作用:汇编器将hello.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在hello.o目标文件中,hello.o文件是一个二进制文件,它包含程序的指令编码。

4.2 在Ubuntu下汇编的命令
在这里插入图片描述
图十六:输入汇编命令

输入命令:gcc –c hello.s –o hello.o

4.3 可重定位目标elf格式
1、键入readelf -h hello.o命令,得到ELF Header。
在这里插入图片描述
图十七:ELF Header

ELF Header:以 16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括 ELF 头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如 x86-64)、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。此外,根据图十七,我们可以知道可以知道该文件是可重定位目标文件,有14个节。

2、键入readelf -S hello.o命令,得到Section Headers。
在这里插入图片描述
图十八:Section Headers
Section Headers:描述不同节的位置和大小,包括节名称,节的类型,节的属性(读写权限),节在ELF文件中所占的长度,节的对齐方式以及节的偏移量。

3、键入readelf -s hello.o命令,得到符号表.symtab。
在这里插入图片描述
图十九:符号表.symtab

符号表.symtab:存放在程序中定义和引用的函数和全局变量的信息,但它不包含局部变量的条目。

4、键入readelf -a hello.o > ./elf.txt,得到重定位节。
在这里插入图片描述
图二十:重定位节

重定位节.rela.text:一个.text节中位置的列表,当链接器把这个文件和其他文件组合时,需要修改这些位置。
重定位节.rela.data:被模块引用或定义的所有全局变量的重定位信息。
偏移量:需要被修改的引用节的偏移信息,包括symbol和type两个部分,symbol为前面四个字节,type为后面四个字节。
symbol:标识被修改引用应该指向的符号。
type:重定位的类型。
类型:告知链接器应该如何修改新的应用。
加数:一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整。
符号名称:重定向到的目标的名称。

4.4 Hello.o的结果解析
键入objdump -d -r hello.o,获得如下图所示结果:
在这里插入图片描述
图二十一:hello.o的反汇编(1)
在这里插入图片描述
图二十二:hello.o的反汇编(2)

分析hello.o的反汇编,并与第3章的 hello.s进行对照分析:
hello.o的反汇编相较于hello.s多了机器代码。机器指令由操作码和操作数构成,每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言(操作码和操作数)和二进制机器语言建立一一映射的关系。
操作数:hello.s中的操作数时十进制,hello.o反汇编代码中的操作数是十六进制。
分支转移:跳转语句之后,hello.s中是.L2和.LC1等段名称,而反汇编代码中跳转指令之后是相对偏移的地址,也即间接地址。因为段名称只是在汇编语言中便于编写的助记符。
函数调用:在.s 文件中,函数调用之后直接跟着函数名称,而在反汇编程 序中,call的目标地址是当前指令的下一条指令。因为函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。等待静态链接的进一步确定。

4.5 本章小结
本章先简要介绍汇编的概念和作用,然后具体分析elf格式的可重定位目标文件的各个部分(如ELF Header等等),最后通过比较hello.s与hello.o的反汇编,更加深入地理解了汇编。

第5章 链接

5.1 链接的概念与作用
概念:将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
作用:链接使得分离编译成为可能。我们可以将一个大型应用程序分解为更小更好管理的模块,可以独立地修改和编译这些模块,节省了大量的工作空间。

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的elf格式
键入命令:readelf -a hello > hello1.elf得到hello的elf格式文件。
ELF Header:其与.o文件的差别仅仅是类型(可执行文件)与节数(27)不同
在这里插入图片描述
图二十四:ELF Header

Section Headers:描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。其中地址是程序被载入到虚拟地址的起始地址。
在这里插入图片描述
图二十五:Section Headers(1)
在这里插入图片描述
图二十六:Section Headers(2)

符号表.dynsym和.symtab:
在这里插入图片描述
图二十七:.dynsym和.symtab

在这里插入图片描述
图二十八:.dynsym和.symtab

重定位节:.rela.dyn和.rela.plt:
在这里插入图片描述
图二十九:.rela.dyn和.rela.plt

5.4 hello的虚拟地址空间
使用edb加载hello,Data Dump 窗口可以查看加载到虚拟地址中的 hello 程序。查看 ELF 格式文件中的 Program Headers,它告诉链接器运行时加载的内容,并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的各方面的信息。在下面可以看出,程序包含PHDR,INTERP,LOAD ,DYNAMIC,NOTE ,GNU_STACK,GNU_RELRO几个部分,如下图所示。
在这里插入图片描述
图三十:edb查看hello

在这里插入图片描述
图三十一:Program Headers

在这里插入图片描述
图三十二:Linux 运行时内存映像

其中PHDR 保存程序头表。INTERP 指定在程序已经从可执行文件映射到内存之后,必须调用的解释器。LOAD 表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据、程序的目标代码等。DYNAMIC 保存了由动态链接器使用的信息。NOTE 保存辅助信息。GNU_STACK:权限标志,用于标志栈是否是可执行。GNU_RELRO:指定在重定位结束之后哪些内存区域是需要设置只读。

在这里插入图片描述
图三十三:部分虚拟地址空间

结合图三十可知hello的虚拟地址空间开始于0x401000,结束于0x401ff0。

5.5 链接的重定位过程分析
键入objdump -d -r hello,得到hello的反汇编代码,通过对比hello与hello.o,可以发现两者有以下不同:
在这里插入图片描述
图三十四:hello的部分反汇编代码

1、对比图三十四与图二十一,我们可以发现hello反汇编的代码有确定的虚拟地址,也就是说已经完成了重定位,而hello.o反汇编代码中代码的虚拟地址均为0,未完成可重定位的过程。由此,hello.o文件中有重定位条目而hello文件中没有。
2、hello相较于hello.o增加了新的节(.init和.plt)和函数(如exit、printf等库函数)。
在这里插入图片描述
图三十五:hello新增的部分函数

hello重定位过程:
1、链接器将所有相同类型的节合并成同一类型的新的聚合节。然后将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。这时,程序中的每条指令和全局变量都有唯一的运行时内存地址。
2、链接器利用可重定位目标模块中称为重定位条目的数据结构,修改节代码和数据节中对每个符号的引用,使得它们指向正确的运行时地址。
3、重定位条目:汇编器遇到对最终位置未知的目标引用时,就会生成一个重定位条目,告诉链接器如何修改这个引用。代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中。

以printf函数为例,由图二十可知,printf的偏移量为0x5e,类型为R_X86_64_PLT32,根据下图公式,可知选择上面的条件分支。
在这里插入图片描述
图三十六:重定位过程的地址计算算法

由图二十五和图二十六可知ADDR(s)=0x401090,r.offset=0x5e,则refaddr=0x4010ee。由图三十五可知ADDR(r.symbol)=0x4010a0。则*refptr=(unsigned)(0x4010a0+(-4)-0x4010ee)=0xffffff19,符合公式。

5.6 hello的执行流程
使用edb执行hello,从加载hello到_start,到call main,以及程序终止的所有过程。以下是其调用与跳转的各个子程序名和程序地址。

程序名称 程序地址
ld -2.31.so!_dl_catch_exception@plt 0x7fec8b285010
ld -2.31.so!malloc@plt 0x7fec8b285020
ld -2.31.so!_dl_signal_exception@plt 0x7fec8b285030
ld -2.31.so!calloc@plt 0x7fec8b285040
ld -2.31.so!realloc@plt 0x7fec8b285050
ld -2.31.so!_dl_signal_error@plt 0x7fec8b285060
ld -2.31.so!_dl_catch_error@plt 0x7fec8b285070
ld -2.31.so!_dl_find_dso_for_object 0x7fec8b299480
ld -2.31.so!_dl_allocate_tls_init 0x7fec8b298770
ld -2.31.so!__get_cpu_features 0x7fec8b29edf0
ld -2.31.so!__libc_start_main 0x7fec8b29ee20
hello!_init 0x401000
hello!puts@plt 0x401030
hello!strtol@plt 0x401040
hello!__printf_chk@plt 0x401050
hello!exit@plt 0x401060
hello!sleep@plt 0x401070
hello!getc@plt 0x401080
hello!.plt+0x70 0x401090
hello!_start 0x4010f0
hello!_dl_relocate_static_pie 0x401120
hello!deregister_tm_clones 0x401130
hello!main 0x4011d6

5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时,还是需要用到动态链接库。
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时。延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。

由图二十六可知GOT起始表位置为0x404000。在edb中进入该地址可知GOT表位置在调用dl_init之前0x404000后的16个字节均为0。

在这里插入图片描述
图三十七: edb执行init之前的地址

edb在执行_init之后这两个八字节变为0x7fe716209190和0x7fe7161f2ae0,其中GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数。
在这里插入图片描述
图三十八:edb执行init之后的地址

对于变量而言,我们利用代码段和数据段的相对位置不变的原则计算正确地址。对于库函数而言,需要PLT、GOT合作,PLT初始存的是一批代码,它们跳转到GOT所指示的位置,然后调用链接器。初始时GOT里面存的都是PLT的第二条指令,随后链接器修改GOT,下一次再调用PLT时,指向的就是正确的内存地址。PLT就能跳转到正确的区域。

5.8 本章小结
本章主要介绍了链接的概念与作用,并且详细阐述了hello.o是怎么链接成为一个可执行目标文件的过程,详细介绍了hello.o的ELF格式和各个节的含义,并且分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。加深了我对链接的理解。

第6章 hello进程管理

6.1 进程的概念与作用
概念:进程就是一个执行中程序的实例。系统中每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态所组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
作用:
1、运行程序时,shell会创建一个新的进程,然后在这个新进程的上下文切换中运行这个可执行目标文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。
2、进程提供给应用程序的关键抽象:一个独立的逻辑控制流,好像程序独占处理器;一个私有的地址空间,好像程序独占内存系统。

6.2 简述壳Shell-bash的作用与处理流程
作用:shell是一个交互型的应用级程序,它代表用户运行其他程序,解释命令,连接用户和操作系统以及内核。
处理流程:
1、读取用户由键盘输入的命令行。
2、分析命令,以命令名作为文件名,并将其它参数改造为系统调用execve( )内部处理所要求的形式。
3、检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
4、如果不是内部命令,终端进程调用fork( )建立一个子进程。
5、终端进程本身调用waitpid()来等待子进程完成(如果是后台命令,则不等待)。当子进程运行时调用execve(),子进程根据文件名到目录中查找有关文件,调入内存,执行这个程序。
6、如果命令末尾有&,则终端进程不用执行系统调用waitpid(),立即发提示符,让用户输入下一条命令;否则终端进程会一直等待,当子进程完成工作后,向父进程报告,此时中断进程醒来,作必要的判别工作后,终端发出命令提示符,重复上述处理过程。

6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的运行的子进程。父进程判断输入命令执行hello不是内部指令,创造一个子进程子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的 逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。

6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行hello程序,且带参数列表argv和环境变量envp。在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,其中包含以下几个步骤:
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
(2)映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。如图6.4
(3)映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
(4)设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
在这里插入图片描述
图三十九:加载器映射用户地址空间的区域

除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据 复制。直到 CPU 引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用 它的页面调度机制自动将页面从磁盘传送到内存。

6.5 Hello的进程执行
进程提供给应用程序的抽象:
(1) 一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器。
(2) 一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用CPU内存。
1、逻辑控制流:如果想用调试器单步执行程序,我们会看到一系列的程序计数器的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流。进程是轮流 使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占 (暂时挂起),然后轮到其他进程。
2、并发流:一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发地运行。多个流并发地执行的一般现象被称为并发。
3、时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
4、私有地址空间:进程为每个流都提供一种假象,好像它是独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个内存字节是不能被其他进程读或者写的,在这个意义上,这个地址空间是私有的。
5、用户模式和内核模式::处理器通常是用某个控制寄存器中的一个模式位来提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
6、上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。
7、上下文切换:当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程:
1)保存以前进程的上下文;
2)恢复新恢复进程被保存的上下文;
3)将控制传递给这 个新恢复的进程 ,来完成上下文切换。

下面来看hello的进程执行。最初hello运行在用户模式下,输出edb --run ./hello 120L022308 ydh 3,然后hello调用sleep函数之后进程陷入内核模式,内核处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,hello进程继续进行自己的控制逻辑流。
在这里插入图片描述
图四十:输入调用edb的命令

在这里插入图片描述
图四十一:sleep进程上下文切换(图中为read)

然后进入edb界面,执行已经生成的可执行目标文件hello,得到以下结果:
在这里插入图片描述
图四十二:程序执行结果

6.6 hello的异常与信号处理
异常就是控制流中的突变,用来响应处理器状态中的某些变化,它可以分为四类:
1、中断:中断是处理来自I/O设备的信号的结果,是异步发生的,总是返回到下一条指令。
2、陷阱:它是有意的异常,是执行一条指令的结果,它是同步发生的,总是返回到下一条指令。
3、故障:是由错误情况引起,它可能能够被故障处理程序修正。它是同步发生的,并且可能返回到当前指令。
4、终止:是不可恢复的致命错误造成的结果,通常是一些硬件错误。它也是同步的,且不会返回。
信号是更高层的软件形式的异常,它就是一条小消息,通知进程系统中发生了一个某种类型的事件。它提供了一种机制,通知用户进程发生了异常。Linux信号主要有以下几种:
在这里插入图片描述
图四十三:Linux信号

在键入./hello 120L022308 ydh 1运行hello时,正常情况在图四十二中已经体现出来了。此时我们通过键盘键入ctrl-z,键入ctrl-z的默认结果是挂起前台的作业,hello进程并没有回收,而是运行在后台下,如图四十五所示用ps命令可以看到,hello进程并没有被回收。此时他的后台 job 号是 1,调用 fg 1将其调到前台,此时 shell 程序首先打印 hello 的命令行命令, hello 继续运行打印剩下的 8 条 info,之后输入字串,程序结束,同时进程被回收。

在这里插入图片描述
图四十四:键入ctrl-z

在这里插入图片描述
图四十五:键入ps命令

在这里插入图片描述
图四十六:键入jobs查看后台job号

在这里插入图片描述
图四十七:调用 fg 1 将hello调到前台

然后我们重新运行hello,并测试输入ctrl-c会产生什么结果。在键盘上输入ctrl-c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业。图四十八反映了与输入ctrl-z的不同,可见ctrl-c直接结束了hello。

在这里插入图片描述
图四十八:键入ctrl-c以及后续检查

下面运行kill指令:挂起的进程被终止,在ps中无法查到到其PID。
在这里插入图片描述
图四十九:键入kill命令

如果在终端乱按,可以发现,乱按只是将屏幕的输入缓存到 stdin,当 getchar 的时候读出一个’\n’结尾的字串(作为一次输入),其他字串会当做 shell 命令行输入:
在这里插入图片描述
图五十:不停乱按的结果

6.7本章小结
本章主要了阐述进程的定义与作用,同时介绍了 Shell 的一般处理流程和作用,并且着重分析了调用 fork 创建新进程,调用 execve函数 执行 hello,hello的进程执行,以及hello 的异常与信号处理,加深了我对异常与信号处理的理解。

第7章 hello的存储管理

7.1 hello的存储器地址空间
逻辑地址:包含在机器语言指令中用来指定一个操作数或一条指令的地址,这种寻址方式在80x86著名的分段结构中表现得尤为具体,它促使windows程序员把程序分成若干段。每个逻辑地址都由一个段和偏移量组成,偏移量指明了从段开始的地方到实际地址之间的距离。例如我们常说的结构体中某个参数的地址其实就相当于:结构体首地址 + 偏移量。逻辑地址是相对于应用程序而言的。
线性地址:是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。是一个32位无符号整数,可以用来表示高达4GB的地址,也就是,高达4294967296个内存单元。线性地址通常用十六进制数字表示,值的范围从0x00000000到0xfffffff。
虚拟地址:即线性地址。
物理地址:CPU地址总线传来的地址,由硬件电路控制(现在这些硬件是可编程的了)其具体含义。物理地址中很大一部分是留给内存条中的内存的,但也常被映射到其他存储器上(如显存、BIOS等)。可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽象。

7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节,表示具体的是代码段寄存器还是栈段寄存器抑或是数据段寄存器。
在这里插入图片描述
图五十一:段选择符

索引号就是“段描述符(segment descriptor)”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这句话很关键,说明段标识符的具体作用,每一个段描述符由8个字节组成。
在这里插入图片描述
图五十二:段描述符

Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。给定一个完整的逻辑地址段选择符+段内偏移地址,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符。
在这里插入图片描述
图五十三:GDT

总的来说,就是被选中的段描述符先被送至描述符cache,每次从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址。
在这里插入图片描述
图五十四:逻辑地址转换到线性地址

7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(也就是虚拟地址 VA)到物理地址(PA)之间的转换通过分页机制完成。而分页机制是对虚拟地址内存空间进行分页。
n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,例如VPN 0选择PTE 0。根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。这里的VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。
在这里插入图片描述
图五十五:使用页表的地址翻译

页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
在这里插入图片描述
图五十六:页式管理流程图

7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。
多级页表:将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。
在这里插入图片描述
图五十七:使用k级页表进行翻译

CPU 产生虚拟地址 VA,VA 传送给 MMU,MMU 使用前 36 位 VPN 作为 TLBT(前 32 位)+TLBI(后 4 位)向 TLB 中匹配,如果命中,则得到 PPN (40bit)与 VPO(12bit)组合成 PA(52bit)。 如果 TLB 中没有命中,MMU 向页表中查询,CR3 确定第一级页表的起始地 址,VPN1(9bit)确定在第一级页表中的偏移量,查询出 PTE,如果在物理内存 中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查 询到 PPN,与 VPO 组合成 PA,并且向 TLB 中添加条目。如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
解析VA,利用前m位vpn1寻找一级页表位置,接着一次重复k次,在第k级页表获得了页表条目,将PPN与VPO组合获得PA。

7.5 三级Cache支持下的物理内存访问
CPU发送一条虚拟地址,随后MMU按照上述操作获得了物理地址PA。根据cache大小组数的要求,将PA分为CT(标记位)CS(组号),CO(偏移量)。根据CS寻找到正确的组,比较每一个cacheline是否标记位有效以及CT是否相等。如果命中就直接返回想要的数据,如果不命中,就依次去L2,L3,主存判断是否命中,当命中时,将数据传给CPU同时更新各级cache的cacheline(如果cache已满则要采用换入换出策略)。
Cashe的物理访存大致过程如下:
(1) 组选择取出虚拟地址的组索引位,将二进制组索引转化为一个无符号整数,找到相应的组
(2) 行匹配把虚拟地址的标记为拿去和相应的组中所有行的标记位进行比较,当虚拟地址的标记位和高速缓存行的标记位匹配时,而且高速缓存行的有效位是1,则高速缓存命中。
(3) 字选择一旦高速缓存命中,我们就知道我们要找的字节在这个块的某个地方。因此块偏移位提供了第一个字节的偏移。把这个字节的内容取出返回给CPU即可
(4)不命中如果高速缓存不命中,那么需要从存储层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位所指示的组中的一个高速缓存行中。一种简单的 放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块, 产生冲突(evict),则采用最近最少使用策略 LFU 进行替换。
在这里插入图片描述
图五十八:3级Cache

7.6 hello进程fork时的内存映射
当 fork 函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给 它一个唯一的 PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。因此,也就为每个进程保持了私有空间地址的抽象概念。

7.7 hello进程execve时的内存映射
execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。 加载并运行 hello 需要以下几个步骤:
1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存 在的区域结构。
2)映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结 构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名 文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长 度为零。
3)映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链 接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4)设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程 上下文的程序计数器,使之指向代码区域的入口点。

7.8 缺页故障与缺页中断处理
缺页故障:当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的。整体的处理流程:处理器生成一个虚拟地址,并将它传送给MMU。MMU生成PTE地址,并从高速缓存/主存请求得到它,高速缓存/主存向MMU返回PTE。PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。缺页处理程序页面调入新的页面,并更新内存中的PTE。缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。
在这里插入图片描述
图五十九:缺页故障处理流程

7.9动态存储分配管理
动态内存分配器维护者一个进程的虚拟内存区域,成为堆。分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。分配器有两种基本风格。两种风格都是要求显示的释放分配块。
(1) 显式分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。
(2) 隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。
动态内存分配主要有两种基本方法与策略:
1、带边界标签的隐式空闲链表分配器管理
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。
隐式空闲链表:在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。
2、显示空间链表管理
显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。
显式空闲链表:在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。

7.10本章小结
本章主要介绍了hello的存储器的地址空间,介绍了四种地址空间的差别和地址的相互转换。同时介绍了hello的四级页表的虚拟地址空间到物理地址的转换。阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法
设备的模型化:文件。一个Linux文件就是一个m字节的序列(所有的I/O设备都被模型化为文件,甚至内核也被映射为文件)。
设备管理:unix io接口。所有的 IO 设备(如网路、磁盘、终端)都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行。我们可以对文件的操作有:打开关闭操作open和close;读写操作read和write;改变当前文件位置lseek等。

8.2 简述Unix IO接口及其函数
Unix I/O 接口:
(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想 要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在 后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文 件的所有信息。
(2)Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标 准错误。
(3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。
(4)读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文 件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文 件,当 k>=m 时,触发 EOF。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。
(5)关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢 复到可用的描述符池中去。
Unix I/O 函数:
(1)int open(char* filename,int flags,mode_t mode) ,进程通过调用 open 函 数来打开一个存在的文件或是创建一个新文件的。 open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在 进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访 问这个文件,mode 参数指定了新文件的访问权限位。
(2)int close(fd),fd 是需要关闭的文件的描述符,close 返回操作结果。
(3) ssize_t read(int fd,void *buf,size_t n),read 函数从描述符为 fd 的当前文 件位置赋值最多 n 个字节到内存位置 buf。返回值-1 表示一个错误,0 表示 EOF,否则返回值表示的是实际传送的字节数量。
4) ssize_t wirte(int fd,const void *buf,size_t n),write 函数从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置。

8.3 printf的实现分析
printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。在printf中调用系统函数write(buf,i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。于是我们的打印字符串就显示在了屏幕上。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
在这里插入图片描述图六十:printf、write以及syscall函数体

8.4 getchar的实现分析
getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
在这里插入图片描述
图六十一:getchar函数体

8.5本章小结
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数的实现。

结论

逐条总结hello所经历的过程:
1、hello.c经过预编译,拓展得到hello.i文本文件
2、hello.i经过编译,得到汇编代码hello.s汇编文件
3、hello.s经过汇编,得到二进制可重定位目标文件hello.o
4、hello.o经过链接,生成了可执行文件hello
5、bash进程调用fork函数,生成子进程;并由execve函数加载运行当前进程的上下文中加载并运行新程序hello
6、hello的变化过程中,会有各种地址,但最终我们真正期待的是PA物理地址。
7、hello再运行时会调用一些函数,比如printf函数,这些函数与linux I/O的设备模拟化密切相关
8、hello最终被shell父进程回收,内核会收回为其创建的所有信息

计算机系统的设计与实现是天才般的行为,是顶聪明的人造出来给我等凡人使用和浅学的。CSAPP这本书也是令人敬佩,深入浅出,内容涵盖广泛,是这一领域的权威之作。

附件

文件名 文件作用
hello.i hello.c预处理得到的文本文件
hello.s hello.i编译而成的汇编程序
hello.o hello.s汇编而成的可重定位目标程序
hello hello.o链接成可执行目标文件
hello.out hello反汇编而成的反汇编文件
elf.txt hello.o的ELF 格式
hello1.elf hello的ELF 格式

参考文献

[1] 《深入理解计算机系统》 Randal E.Bryant David R.O’Hallaron 机械工业出版社
[2] 博客园 printf函数实现的深入剖析
[3] CSDN博客 Ubuntu系统预处理、编译、汇编、链接指令
[4] 博客园 从汇编层面看函数调用的实现原理
[5] CSDN博客 ELF可重定位目标文件格式
[6] 博客园 shell命令执行过程
[7] 《步步惊芯——软核处理器内部设计分析》 TLB的作用及工作过程
[8] 博客园 [转]printf 函数实现的深入剖析录可以自动生成,如何生成可参考右边的帮助文档

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
哈尔滨工业大学(Harbin Institute of Technology,简称“哈工大”)是中国著名的重点大学,成立于1920年,是中国最早创办的六所工科高等学府之一。其中,哈尔滨工业大学的计算机科学与技术学院一直以来都是国内知名的学院。在其中,CSAPP是哈工大计算机科学与技术学院开设的一门经典课程,全称为《深入理解计算机系统》(Computer Systems: A Programmer's Perspective)。 这门课程涵盖了计算机系统的各个方面,从高级语言编程到机器级别的细节都有涉及,深入剖析了计算机系统的内部机制,讲解了各种计算机组件的原理,如内存、处理器、I/O设备、网络等等。此外,课程内容还包括缓存、异常、程序优化、并发编程、虚拟内存等重要主题,并且还会涉及安全问题,例如注入攻击、缓冲区溢出等等。 相较于其他计算机相关的课程而言,CSAPP的特殊之处在于,它以程序员的视角,深入而生动地解释了计算机系统的工作方式和内部机制。课程强调了实践性,通过大量的例子及编程作业,学生可以实际操作并理解到具体的计算机系统的运行方式。 此外,CSAPP的教学团队非常强大,由哈工大的多顶尖教授组成,能够保证教学质量和深度。学生通过学习这门课程,不仅可以深入了解计算机系统的各个方面,还可以提高编程能力和工程实践水平,有助于更好地应对工作中遇到的各种问题。 总之,CSAPP是哈尔滨工业大学计算机科学与技术学院开设的一门经典课程,其全面而深入的课程内容、强调实践性、优秀的教学团队等特色让其在国内享有较高声誉,对学生深入理解计算机系统、提高编程实践能力等方面,都有非常积极的作用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值