大作业-120L020904-王子豪

在这里插入图片描述

计算机系统

大作业

题 目 程序人生-Hello’s P2P

专 业 计算学部

学 号 120L020904

班 级 2003001

学 生 王子豪

指 导 教 师 史先俊

计算机科学与技术学院

2022年5月

摘 要

Hello的一生是平凡的程序的一生,但也是见证计算机系统精妙设计的一生。我们将从其如何从.c文件变为计算机运行中的一个进程的过程讲起,同时介绍在这个过程中存储器与系统IO的作用。通过对hello一生的了解,我们将回顾.c文件到可执行文件的四个阶段,进程管理与内存管理的机制,以及系统IO如何运行。本文将会以hello的角度再现深入理解计算机系统的大部分内容。

**关键词:**重定位;编译;链接;虚拟内存;进程管理;IO管理

**
**

目 录

第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程序经过预处理器预处理产生.i文件,编译器编译产生.s文件,汇编器产生.o文件即可重定位目标文件,链接器与库函数链接产生可执行文件,命令行输入./hello后shell调用fork函数为hello程序创建进程,执行hello程序。

020(from 0 to 0):hello程序初始时没有占用内存与CPU资源,之后在shell执行程序时进行虚拟内存映射与物理内存载入,CPU执行程序的每一条指令,在进程终止后向父进程发送信号SIGCHILD,父进程调用信号处理程序回收清除hello进程所有的痕迹,使其不再占用内存与CPU资源,就好像没有存在过一样。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

  1. 硬件环境

CPU: Intel® Core™ i7-8565U CPU @ 1.80GHz

RAM: 16.00GB

  1. 软件环境

Windows10 64位

VMware® Workstation 16 Pro 16.2.3 build-19376536

Ubuntu 20.04.1

  1. 开发与调试工具

Visual Studio Code

cpp(预处理器)

gcc(编译器)

as(汇编器)

ld(链接器)

GNU readelf

GNU gdb

1.3 中间结果

文件名称说明对应本文章节
hello.ihello.c经预处理得到的文本文件第2章
hello.shello.i经编译得到的汇编代码文本文件第3章
hello.ohello.s经汇编得到的可重定位目标文件第4章
hello_elf.txthello.o经readelf分析得到的文本文件第4章
hellohello.o经链接得到的可执行文件第5章
hello1_elf.txthello经readelf分析得到的文本文件第5章

1.4 本章小结

本章为hello一生的概要,主要介绍了hello一生的答题框架,解释了P2P与020的含义,为之后的具体介绍指出方向。

第2章 预处理

2.1 预处理的概念与作用

  1. 概念

预处理器是在编译前进行的处理,预处理器 (cpp) 根据以字符#开头的命令,修改原始的C程序。

  1. 作用
  2. 预处理可以进行宏展开,例如在程序开头写的#define,预处理会用define之后的内容替换define后的字符。
  3. 预处理会将include指向的文件插入到程序文本中,例如本次程序的三个include文件会在预处理时插入到车光绪文本中。
  4. 可以利用预处理进行库打桩。
  5. 预处理可以实现条件编译。

2.2在Ubuntu下预处理的命令

预处理命令

  1. gcc –E hello.c –o hello.i
  2. cpp hello.c > hello.i

在这里插入图片描述

图 1预处理命令

在这里插入图片描述

图 2预处理后的.i文件

2.3 Hello的预处理结果解析

程序的预处理不改变程序的原码,在这里预处理只是将头文件插入到程序文本中并删除了原程序文本的注释,程序由原来的23行增加到3060行,多出来的部分就是插入的头文件。每次替换头文件时预处理文件会将其地址显示出来。这里预处理器之所以能够找到头文件的所在位置是因为调用标准库的话会在系统环境变量下的相应目录寻找。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qULk37AJ-1652891467355)(media/f45bffb7729cfc30ea24e37965a9b968.png)]

图 3预处理文件中与头文件地址有关的部分

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fiQI3D7k-1652891467356)(media/94360986d9431138674c6198005d507f.png)]

图 4预处理文件没有更改原程序

2.4 本章小结

本章介绍了预处理的作用,解析了预处理的结果,着重分析了hello.i文本。可以看到,预处理删除了注释与无用的空格,在程序文本中插入了include的文件,解决了条件编译,为之后编译做准备。

第3章 编译

3.1 编译的概念与作用

3.1.1 编译的概念

编译是将预处理得到的.i文件处理为由汇编语言构成的.s文件的过程。

3.1.2编译的作用

编译共有六个阶段,每个阶段都有其作用:

  • 词法分析:是将字符序列转换为单词序列的过程。把字符串形式的源程序改造成为单词符号串形式的中间程序。
  • 语法分析:语法分析是编译过程的一个逻辑阶段。语法分析的任务是在词法分析的基础上将单词序列组合成各类语法短语,并检查是否符合语法。
  • 语义分析:按照语法树的层次关系和先后顺序,进⾏类型审查,审查每个程序是否符合语法规范,不符合时应报告错误。
  • 中间代码生成:在语法和语义分析完成后,将源程序变换成一种“内部表示形式”,该代码是一种简单的记号系统,三元组或者四元组。使程序的逻辑结构更加明确
  • 代码优化:对中间代码进行变换使代码更加高效。
  • 目标代码:将中间代码变换成特定机器上的绝对指令或者可重定位的汇编指令代码。以便机器的执行。

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RS0m6isE-1652891467356)(media/a4a689776012f4453e5fbe6ac75e35e3.png)]

图 5 编译过程与结果

3.3 Hello的编译结果解析

3.3.1 数据与赋值

1. printf语句中的格式串会存储在只读数据.rodata中。

汇编程序代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2fAI7X1e-1652891467356)
(media/7f29549bafbafe7299d3ccfcba6a25ef.png)]

图 6 .s文件中的.rodata段

原程序对应代码:

printf(“用法: Hello 学号 姓名 秒数!\n”);

printf(“Hello %s %s\n”,argv[1],argv[2]);

2. 汇编程序将%rdi与%rsi两个向函数传递参数的寄存器腾出来放入栈中,供其他函数使用。

汇编程序代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MpOF8E4t-1652891467357)(media/9b9093309f0bed2fcf5f40e70a721807.png)]

图 7 汇编代码段2

原程序代码:

int main(int argc,char *argv[]){

  1. 汇编程序将for循环中的计数变量i放入栈中,并赋值为0,在源程序中i为int类型,而在汇编程序中i转化为了long类型。

汇编程序代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RO9c5dmy-1652891467357)(media/721fc2346552e948609fcf271a57a931.png)]

图 8 汇编代码段3

原程序代码:

for(i=0;i<8;i++){

  1. 算术运算

  2. 汇编程序中算术运算为循环变量的加一操作与数组访问过程中的地址计算。(有关数组访问的部分将在数组访问部分介绍)

    汇编程序代码:

在这里插入图片描述

图 9 循环变量加一

原程序代码:

for(i=0;i<8;i++){

  1. 数组与指针

  2. 原程序中有三处数组访问,在汇编程序中都以访问栈中数组体现出来。在赋值与数据一节中我们已经知道,输入主函数的数组的首地址被存放在栈中。

    汇编程序代码:

在这里插入图片描述

图 10 访问argv[1]与argv[2]

在这里插入图片描述

图 11 访问argv[3]

原程序代码:

printf(“Hello %s %s\n”,argv[1],argv[2]);

sleep(atoi(argv[3]));

  1. 控制转移

    在这里插入图片描述

图 12 汇编程序中的跳转指令

  1. 原程序有两处控制转移,其中一处为if的控制转移。这里cmpl是比较两个长整型,本质是将两个数相减。

    汇编程序代码:

在这里插入图片描述

图 13 if转移

原程序代码:

if(argc!=4)

  1. 另一处为跳出循环转移,这里jle为小于等于就跳转的意思。

    汇编程序代码:

在这里插入图片描述

图 14 循环跳出判断

原程序代码:

for(i=0;i<8;i++)

  1. 函数

  2. 主函数的参数传入到%rdi与%rsi两个寄存器中,之后转存入栈中。

    汇编程序代码:

在这里插入图片描述

图 15 主函数参数传入

  1. 汇编程序中将第一个printf中的字符串当作参数传递给puts。

    汇编程序代码:

在这里插入图片描述

图 16 puts函数的调用

原程序代码:

printf(“用法: Hello 学号 姓名 秒数!\n”);

  1. 汇编程序将1传入exit并调用exit。

    汇编程序代码:

在这里插入图片描述

图 17 exit函数的调用

原程序代码:

exit(1);

  1. 汇编程序将常数字符串、两个数组元素作为参数传给printf,并调用printf。

    汇编程序的代码:

    在这里插入图片描述

图 18 调用printf函数

原程序代码:

printf(“Hello %s %s\n”,argv[1],argv[2]);

  1. 汇编程序将字符转化为整数后传递给sleep函数,并调用sleep。

    汇编程序的代码:

在这里插入图片描述

图 19 调用sleep函数

原程序代码:sleep(atoi(argv[3]));

3.4 本章小结

编译器会将预处理后的文件转换为与原程序等价的汇编语言程序。本章主要分析了汇编语言程序在数据与赋值、算术运算、控制转移、函数调用、数组指针等方面与原程序的关系。编译后的汇编程序为之后的汇编器生成可重定位目标文件奠定了基础。在这部分的分析中使我对汇编程序的理解更加的深刻。

第4章 汇编

4.1 汇编的概念与作用

4.1.1 概念

汇编器as,将汇编语言程序(hello.s)翻译成机器语言指令,并将这些指令打包成可重定位目标文件(hello.o)的过程称为汇编。hello.o是二进制编码文件,包含程序的机器指令编码。

  1. 作用

汇编器生成的可重定位目标文件已经非常接近最后的可执行文件,其中的重定位条目与符号表为之后的链接做铺垫。

4.2 在Ubuntu下汇编的命令

gcc -c hello.s -o hello.o

as hello.s -o hello.o

在这里插入图片描述

图 20 .o文件的生成

4.3 可重定位目标elf格式

4.3.1 ELF格式文件的生成

命令:readelf -a hello.o > hello_elf.txt

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qt1jwYdN-1652891467362)(media/cbb7d1c79d4cb5e63769edf66da26fe8.png)]

图 21 ELF格式文件

4.3.2 ELF头

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ek0wqUOR-1652891467363)(media/8029a901c161365a10a585011786d490.png)]

图 22 ELF头

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

4.3.3 节头部表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CAxy7DQ6-1652891467364)(media/cde7b3a482ee23117f923e883e781dd6.png)]

图 23 节头部表

**分析:**不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。这个条目包含了名称、类型、地址、偏移量、大小、全体大小、旗标、链接与对齐的信息。

4.3.4 重定位节

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CMjDHQoT-1652891467364)(media/8b9a4d68acc7ec325c64a78ef46263dd.png)]

图 24 重定位节

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PJU7Rw3g-1652891467365)(media/6f4eb3c16db213fa742351861d9de9c1.png)]

图 25 重定位算法

**分析:**偏移量(offset)是需要被修改的引用的节偏移。符号名称(symbol)标识被修改引用应该指向的符号。类型(type)告知链接器如何修改新的引用。加数(addend)是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。在这里我们可以看到.rodata用到了PC32位相对地址引用。其他函数则用的是R_X86_64_PLT32即PLT表寻址。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WEbYAPhX-1652891467366)(media/3a5998b2cf5b1a0a973c626a023aba70.png)]

图 26 符号表

分析:.symtab节中包含ELF符号表。这张符号表包含一个条目的数组。name 是字符串表中的字节偏移,指向符号的以null结尾的字符串名字。value是符号的地址。对于可重定位的模块来说,value 是距定义目标的节的起始位置的偏移。对于可执行目标文件来说,该值是一个绝对运行时地址。size是目标的大小(以字节为单位)。type通常要么是数据,要么是函数。符号表还可以包含各个节的条目,以及对应原始源文件的路径名的条目。所以这些目标的类型也有所不同。binding 字段表示符号是本地的还是全局的。

4.4 Hello.o的结果解析

**命令:**objdump -d -r hello.o

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jaln0af7-1652891467366)(media/bbef25e4b985c7873ac49efc982f6418.png)]

图 27 反汇编代码

**分析:**hello.s与hello.o大体相同,主要不同集中在连接器所做的工作即符号解析与重定位上。经过重定位之后,汇编程序原本对不同节的访问如L2等已经被更换为相对主函数地址访问;原本的函数符号如exit@PLT已经被更换为相对主函数地址访问。在这些被链接器重定位的地方都增加了重定位信息。

4.5 本章小结

本章汇编器对原本的.s文件进行了汇编,主要工作便是生成符号表与重定位条目,将编译形成汇编程序中对函数符号与printf打印的常量进行了重定位,使程序变得接近可执行文件,为之后链接器与库文件链接做了准备。经过这章之后我对汇编器的工作与可重定位目标文件有了更加深刻的认识。

第5章 链接

5.1 链接的概念与作用

5.1.1 链接的概念

链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。

  1. 链接的作用

链接器使得分离编译 (separate compilation) 成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

5.2 在Ubuntu下链接的命令

命令:ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o hello.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o -o hello

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZwJGqNFN-1652891467366)(media/5ae87b73a9f1bcf002a7ddeb1f7c3be1.png)]

图 28 hello文件的生成

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

命令:readelf -a hello > hello1_elf.txt

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eBlsImIR-1652891467367)(media/43d82b78e4779452b3b6306cdb416078.png)]

图 29 ELF头

**分析:**可执行文件的ELF头与可重定位文件的ELF头的格式相同。但有些地方发生了改变如类型由原来的可重定位文件变为了可执行文件,入口点地址不再为0而是变成了0x4010f0。程序头由原来的大小为0字节变为了大小为56字节,数量也由原来的0个变为了12个。节头由原来的14个增加到27个,节头引用也随之发生改变。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Do6jVNW8-1652891467367)(media/ebb353fc2d8cc9426869abaf1c0899f5.png)]

图 30 节头部表1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fXtVMWH9-1652891467367)(media/c7b4fe733a8d5967dbf8023ac651b070.png)]

图 31 节头部表2

**分析:**不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。这个条目包含了名称、类型、地址、偏移量、大小、全体大小、旗标、链接与对齐的信息。

5.4 hello的虚拟地址空间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OBZnZpNO-1652891467367)(media/64485be2de8c8c4f8b3de92ec51b4fbf.png)]

图 32 edb获得的虚拟内存图1

**分析:**该部分由最开始的ASCII码显示为ELF可以看出这与我们在图29展示的ELF头相对应,此处明显为ELF头的信息,通过图30我们可以得知0x400000到0x401000存储的内容分别为ELF头、.interp、.hash、.gnu.hash、.dynsym、.dynstr、

.gnu.version、.gnu.version_r、.rela.dyn、.rela.plt。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MKWv32ud-1652891467368)(media/7f87b63c52fc3d73d093723b8a445508.png)]

图 33 edb获得的虚拟内存图2

**分析:**由图30与图31的节头部表可以得知,此处存放的节有:.init、.plt、.plt.sec、

.text、.fini。

在这里插入图片描述

图 34 edb获得的虚拟内存图3

**分析:**由节头部表可知,该处存放的节有.rodata、.eh_frame。

从ASCII码显示出的hello与%s可知这里存放的是printf的常数字符串参数。由之前的分析知printf的常数字符串参数被储存在.rodata中,因此这里包含了.rodata,这与节头部表给出的信息一致。

在这里插入图片描述

图 35 edb获得的虚拟内存图4

**分析:**由节头部表可知,该处存放的节有.dynamic、.got、.got.plt、.data。

5.5 链接的重定位过程分析

**命令:**objdump -d -r hello

在这里插入图片描述

图 36 可执行文件反汇编之后的结果1

在这里插入图片描述

图 37 可执行文件反汇编之后的结果2

在这里插入图片描述

图 38 可执行文件反汇编之后的结果3

在这里插入图片描述

图 39 可执行文件反汇编之后的结果4

在这里插入图片描述

图 40 可执行文件反汇编之后的结果5

**分析:**主要分析图 41 可执行文件反汇编之后的结果3即主函数部分,通过对比我们发现在.o文件中的相对地址全部被更改为绝对地址<main+偏移>的模式。链接器依赖可重定位目标模块中称为重定位条目 (relocation entry) 的数据结构修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。

以第一处重定位0x40113c中的je跳转地址的重定位为例,在原本的.o文件只给出相对主函数偏移为0x2f,而链接之后通过将其与主函数运行地址相加得出跳转地址为0x401154。其他重定位分析与此相同。

5.6 hello的执行流程

在这里插入图片描述

图 42 使用edb执行hello

**分析:**这里通过查看%rip的值可以得知程序正在调用start函数。

程序名地址
ld-2.31.so!_dl_start0x7f90f3e2edf0
ld-2.31.so!_dl_init0x7f90f3e3ec10
hello!_start0x4010f0
libc-2.31.so!__libc_start_main0x7f90f3c46fc0
hello!_main0x401125
hello!atoi @plt0x4010c0
hello!printf@plt0x4010a0
hello!sleep@plt0x4010e0
hello!getchar@plt0x4010b0
libc-2.31.so!exit0x7ff62fdf5a70

表格 1 子程序名与子程序地址

5.7 Hello的动态链接分析

在这里插入图片描述

图 43 .got与.got.plt在dl_init运行前部分截图

在这里插入图片描述

图 44 .got与.got.plt在dl_init运行后部分截图

**分析:**这里涉及到编译系统的延迟绑定技术,该技术通过GOT与PLT的交互来实现。GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时, GOT [0]和 GOT[l] 包含动态链接器在解析函数地址时会使用的信息。 GOT[2] 是动态链接器在ld-linux.so模块中的入口点。其余的每个条目对应一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。PLT与GOT会协作解析函数的运行地址,以达到动态链接的目的,期间GOT数组的内容会被PLT改变。在这里运行前与运行后GOT数组很明显发生了变化。

5.8 本章小结

本章主要介绍了链接相关的知识,展示了hello.o文件如何在链接器的帮助下生成hello可执行文件。首先我们分析了虚拟地址空间与节头部表的对应关系,随后我们通过执行流程的查看重点分析了其中的重定位与动态链接过程,让我对链接的理解更加的深刻。

第6章 hello进程管理

6.1 进程的概念与作用

6.1.1 进程的概念

进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文 (context)中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

6.1.2 进程的作用

进程可以清晰地刻画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序。

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

6.2.1 作用

shell是操作系统的最外层,是一个用户跟操作系统之间交互的命令解释器。大多数linux默认的shell命令解释器是 bash(/bin/bash)。shell独立于内核,是链接内核和应用程序的桥梁,通俗来讲shell是内核周围的外壳。

6.2.2 处理流程

1.Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |

2. 程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。

3.当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程回到第一步重新分割程序块tokens。

4.Shell对~符号进行替换。

5.Shell对所有前面带有$符号的变量进行替换。

6.Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用$(command)标记法。

7.Shell计算采用$(expression)标记的算术表达式。

8.Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB 和换行符号。

9.Shell执行通配符* ? [ ]的替换。

10.shell把所有从處理的結果中用到的注释删除,並且按照下面的顺序实行命令的检查:

A.内建的命令

B. shell函数(由用户自己定义的)

C.可执行的脚本文件(需要寻找文件和PATH路径)

11.在执行前的最后一步是初始化所有的输入输出重定向。

12.最后,执行命令。

6.3 Hello的fork进程创建过程

在执行命令(./hello 120L020904 王子豪 1)后,shell对输入的命令字符进行处理。之后shell发现其为可执行文件,初始化所有输入输出重定向,调用fork函数创建子进程。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。

6.4 Hello的execve过程

execve 函数加载并运行可执行目标文件hello, 且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到hello, execve才会返回到调用程序。

子进程通过execve系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片(chunk), 新的代码和数据段袚初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用应用程序的main函数。

6.5 Hello的进程执行

操作系统内核使用一种称为上下文切换 (context switch) 的较高层形式的异常控制流来实现多任务。内核为每个进程维持 一个上下文 (context) 。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。

在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换

  1. 保存当前进程的上下文。

  2. 恢复某个先前被抢占的进程被保存的上下文。

  3. 将控制传递给这个新恢复的进程。

在这里插入图片描述

图 45 进程的切换

Hello程序在正常执行时会在调用sleep函数发生进程切换,调度到其他进程。进程A初始运行在用户模式中,直到它通过执行系统调用sleep陷入到内核。在切换之前,内核正代表进程A在用户模式下执行指令(即没有单独的内核进程)。在第一部分切换中,内核代表进程A在内核模式下执行指令。然后在某一时刻,它开始代表进程 B(仍然是内核模式下)执行指令。在切换之后,内核代表进程B在用户模式下执行指令。进程B在用户模式下运行,直到sleep发出一个信号,内核就执行一个从进程B到进程A的上下文切换,将控制返回给进程A中在系统调用sleep之后的那条指令。进程A继续运行,直到下一次异常发生

6.6 hello的异常与信号处理

执行过程截图:

在这里插入图片描述

图 46 正常执行之后的结果

在这里插入图片描述

图 47 键入Ctrl+C之后的结果

**分析:**通过终端我们可以看出程序在运行时接收了SIGINT信号shell会将子进程终止。

在这里插入图片描述

图 48 键入Ctrl+Z之后程序的运行

**分析:**在输入Ctrl+Z之后子进程被挂起,由终端可见其接收了信号SIGTSTP。

在这里插入图片描述

图 49 输入pstree之后的结果

**分析:**可以看到有hello与pstree两个进程。

在这里插入图片描述

图 50 输入jobs命令之后的结果

**分析:**这里只有hello一个作业在终端处于挂起状态。

在这里插入图片描述

图 51 输入ps之后的结果

**分析:**可以看到有hello和ps进程。

在这里插入图片描述

图 52 输入fg命令之后的结果

**分析:**可以发现输入fg之后将被挂起的程序放入前台继续执行。

在这里插入图片描述

图 53 输入kill -9 4521之后的结果

**分析:**kill向进程4521发送命令SIGKILL,hello进程被终止。

6.7本章小结

本章主要从进程的处理方面来看待hello程序,从shell解析执行hello程序的命令到hello进程的创建,接着hello进程的执行,最后讲到hello进程对异常与信号的处理,分析了hello进程的从生到死。这章的分析让我对第八章有了更加深刻的了解。

(第6章1分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

地址空间 (address space) 是一个非负整数地址的有序集合。如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间 (linear address space)。地址空间的概念是很重要的,因为它清楚地区分了数据对象(字节)和它们的属性(地
址)。一旦认识到了这种区别,那么我们就可以将其推广,允许每个数据对象有多个独立的地址,其中每个地址都选自一个不同的地址空间。这就是虚拟内存的 基本思想。主存中的每字节都有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地址。

逻辑地址:逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。如hello.s中jmp .L3 这样的相对跳转地址。逻辑地址并不是真实存在的,它是在段式管理下的产物。

虚拟地址(线性地址):CPU 通过生成一个虚拟地址(Virtual Address, VA)来访问主存,这个虚拟地址在被送到内存之前先转换成适当的物理地址。将 一个虚拟地址转换为物理地址的任务叫做地址翻译(address translation)。我们在可执行文件反汇编之后看到的所有的地址都是虚拟地址。虚拟地址与物理地址建立起映射关系,但其并不是真实存在的。

物理地址:计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址(Physical Address,PA)。第一个字节的地址为0, 接下来的字节地址为1,再下一个为2依此类推。给定这种简单的结构,CPU访问内存的最自然的方式就是使用物理地址。Hello可执行程序在虚拟内存与物理内存建立起映射后程序的执行便是在物理内存上执行的。物理地址是真实存在的。

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

段式存储管理的基本思想:把程序按内容或过程(函数)关系分为段,每段有自己的名字。段式管理程序以段为单位分配内存,然后通过地址映射机构把段式虚拟地址转换成实际的内存物理地址。

段式管理以段为单位分配内存,每段分配一个连续的内存区。由于各段长度不等,所以这些存储区的大小不一。同一进程包含的各段之间不要求连续。段式管理的内存分配与释放在作业或进程的执行过程中动态进行。

在进行初始内存分配之前,首先根据用户要求的内存大小为作业或进程建立一个段表,一般在内存中给出一块固定的区域放置段表。段号与用户指定的段名一一对应。始址和长度分别表示该段在内存或外存的物理地址与实际长度。存取方式对该段进行存取保护。只有处理机状态字中的存取控制位与段表中存取方式一致时才能访问该段。内外用于表明该段现在存储于外存还是内存中。如果所访问段在外存的话,则发生中断。

进程开始执行时,管理程序通过访问段表寄存器得到该进程的段表始址,把段表始址放入段表地址寄存器。依据虚地址中的段号s为索引查段表。若该段在内存,则判断其存取控制方式是否有错。存取控制方式正确,则从段表相应表目中查出该段在内存的起始地址,并将其和段内相对地址w相加,从而得到实际内存地址。如果该段不在内存,则产生缺段中断将CPU控制权交给内存分配程序。内存分配程序首先检查空闲区链,查找是否有足够长度的空闲区装入需要的段。如果内存中的可用空闲区总数小于要求的段的长度,则检查段表中访问位,淘汰那些访问概率低的段并将需要段调入。

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

虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上数组的内容被缓存在主存中。和存储器层次结构中其他缓存一样,磁盘(较低层)上的数据被分割成块,这些块作为磁盘和主存(较高层)之间的传输单元。VM系统通过将虚拟内存分割为称为虚拟页 (Virtual Page, VP)的大小固定的块来处理这个问题。每个虚拟页的大小为P=沪字节。类似地,物理内存被分割为物理页(Physical Page,PP),大小也为P字节(物理页也被称为页帧(page frame))。

在任意时刻,虚拟页面的集合都分为三个不相交的子集:

•未分配的:VM系统还未分配(或者创建)的页。未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间。

•缓存的:当前已缓存在物理内存中的已分配页。

•未缓存的:未缓存在物理内存中的已分配页。

虚拟内存系统借助操作系统软件、 MMU( 内存管理单元)中的地址翻译硬件和一个存放在物理内存中叫做页表 (page table) 的数据结构管理虚拟页。页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。操作系统负责维护页表的内容,以及在磁盘与DRAM之间来回传送页。

页表就是一个页表条目(Page Table Entry, PTE)的数组。虚拟地址空间中的每个页在页表中一个固定偏移处都有一个PTE。我们将假设每个PTE是由一个有效位(valid bit)和一个n位地址字段组成的。设置有效位表示该虚拟页已经被加载到相应的物理内存中,此时地址存放的就是相应的物理内存的起始地址。如果没有设置有效位则表明该虚拟页未被加载到物理内存中或者该虚拟页未被分配。

在这里插入图片描述

图 54 基于页表的地址翻译过程

形式上来说,地址翻译是一个N元素的虚拟地址空间(VAS)中的元素和一个M元素的物理地址空间(PAS)中元素之间的映射。CPU中的一个控制寄存器,页表基址寄存器(Page Table Base Register, PTBR)指向当前页表。n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(Virtual Page Offset, VPO)和一个(n-p)位的虚拟页号(Virtual Page Number, VPN)。MMU利用VPN来选择适当的PTE。将页表条目中物理页号(Physical Page Number, PPN)和虚拟地址中的VPO串联起来,就得到相应的物理地址。

在这里插入图片描述

图 55 地址翻译(正常命中)

  • 第l步:处理器生成一个虚拟地址,并把它传送给MMU。
  • 第2步:MMU生成PTE地址,并从高速缓存/主存请求得到它。
  • 第3步:高速缓存/主存向MMU返回PTE。
  • 第4步:MMU构造物理地址,并把它传送给高速缓存/主存。
  • 第5步:高速缓存/主存返回所请求的数据字给处理器。

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

TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度。用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。如果TLB有T=个组,那么TLB索引(TLBD)是由VPN的t个最低位组成的,而TLB标记(TLBT)是由VPN中剩余的位组成的。

多级页表从两个方面减少了内存要求。以二级页表为例,第一,如果一级页表中的一个PTE是空的,那么相应的二级页表就根本不会存在。这代表着一种巨大的潜在节约,因为对于一个典型的程序,4GB的虚拟地址空间的大部分都会是未分配的。第二,只有一级页表才需要总是在主存中;虚拟内存系统可以在需要时创建、页面调入或调出二级页表,这就减少了主存的压力;只有最经常使用的二级页表才需要缓存在主存中。

用来压缩页表的常用方法是使用层次结构的页表。对于4级页表来说虚拟地址被划分成为4个VPN和1个VPO。每个VPNi都是一个到第i级页表的索引,其中。第j()级页表中的每个PTE,都指向第j+l级的某个页表的基址。第k级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问k个PTE。对于只有一级的页表结构,PPO和VPO是相同的。

在这里插入图片描述

图 56 k级页表的地址翻译

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

高速缓存是一个小而快速的存储设备,它作为存储在更大、也更慢的设备中的数据对象的缓冲区域。使用高速缓存的过程称为缓存。对于每个 k, 位于 K 层的更快更小的存储设备作为位于k+l 层的更大更慢的存储设备的缓存。换句话说,层次结构中的每一层都缓存来自较低一层的数据对象。第k+l层的存储器被划分成连续的数据对象组块,称为块。每个块都有一个唯一的地址或名字,使之区别于其他的块。

在这里插入图片描述

图 57 存储器层次结构原理

当程序需要第k+1层的某个数据对象d时,它首先在当前存储在第k层的一个块中查找d。如果d刚好缓存在第k层中,那么就缓存命中。如果第k层中没有缓存数据对象d,那么就缓存不命中。当发生缓存不命中时,第k层的缓存从第k+l层缓存中取出包含d的那个块,如果第k层的缓存已经满了,可能就会覆盖现存的一个块。覆盖一个现存的块的过程称为替换或驱逐这个块。被驱逐的这个块有时也称为牺牲块。决定该替换哪个块是由缓存的替换策略来控制的。在第k层缓存从第k+l层取出那个块之后,程序就能像前面一样从第k层读出d了。

每个存储器地址有m位,形成个不同的地址。高速缓存被组织成一个有个高速缓存组的数组。每个组包含E个高速缓存行。每个行是由一个字节的数据块组成的。地址中一个有效位指明这个行是否包含有意义的信息,还有个标记位,它们唯一地标识存储在这个高速缓存行中的块。

在这里插入图片描述

图 58 高速缓存的通用组织

在进行高速缓存访问时需要先根据组索引进行组选择,之后根据行标记匹配相应的行,查看有效位是否有效,如果有效则根据块偏移读入相应的字节。

如果有效位表示该行无效或者行标记不匹配,则在下一级高速缓存寻找有效行,如果依然无效或不匹配,则再向下一级寻找,直到找到。之后将相应的行加载到上一级高速缓存空行中,如果上一级高速缓存所有行都非空则需要替换一个有效行。以此类推,直到第一级高速缓存被加载,根据块偏移读取相应的字节传入CPU,完成读取。

7.6 hello进程fork时的内存映射

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

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:

• 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。

• 映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。

• 映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

• 设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

在这里插入图片描述

图 59 加载器映射用户地址空间区域

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

在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页(pagefault)。在发生缺页后会触发一个缺页异常,缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,将新页加载到牺牲页上。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重新发送到地址翻译硬件。

在这里插入图片描述

图60地址翻译(缺页)

  • 第l步:处理器生成一个虚拟地址,并把它传送给MMU。
  • 第2步:MMU生成PTE地址,并从高速缓存/主存请求得到它。
  • 第3步:高速缓存/主存向MMU返回PTE。
  • 第4步:PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
  • 第5步:缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。
  • 第6步:缺页处理程序页面调入新的页面,并更新内存中的PTE。
  • 第7步:缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中。主存就会将所请求字返回给处理器。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk(读做"break"),它指向堆的顶部。

分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

分配器有两种风格,两种风格都要求显式的分配块它们的不同之处在于哪个实体释放已经分配的块。

• 显式分配器要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。

• 隐式分配器要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。

C标准库提供了一个称为malloc程序包的显式分配器。程序通过调用malloc函数来从堆中分配块。malloc函数返回一个指针,指向大小为至少size字节的内存块,这个块会为可能包含在这个块内的任何数据对象类型做对齐。实际中,对齐依赖于编译代码在32位模式(gee-m32)还是64位模式(默认的)中运行。在32位模式中,malloe返回的块的地址总是8的倍数。在64位模式中,该地址总是16的倍数。

如果malloc遇到问题(例如,程序要求的内存块比可用的虚拟内存还要大),那么它就返回NULL,并设置errno。malloc不初始化它返回的内存。

7.10本章小结

本章首先介绍了hello的存储地址空间加深了我对地址空间的了解。之后分别介绍了逻辑地址到物理地址的转与线性地址到物理地址的转换,让我对页式管理与段式管理更加的了解。之后介绍了虚拟内存与物理内存的分级管理,让我更深的理解了分级cache与分级页表。之后介绍了fork函数与execve函数的内存映射。最后分析了动态内存分配与缺页故障。

第8章hello的IO管理

8.1Linux的IO设备管理方法

  1. 设备的模型化:文件

    所有的 I/O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。

    1. 设备管理:unix I/O接口

      这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单、低级的应用接口,称为 Unix 1/ 0, 这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2简述UnixIO接口及其函数

8.2.1 Unix IO

  • 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个1/0设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
  • Linuxshell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STOOUT_FILENO和STDERR_FILENO,它们可用来代替显式的描述符值。
  • 改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
  • 读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k~m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的"EOF符号”。类似地,写操作就是从内存复制n>O个字节到一个文件,从当前文件位置k开始,然后更新k。
  • 关闭文件。当应用完成了对文件的访问之后,就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

8.2.2 Unix IO 函数

  1. int open(char *filename, int flags, mode_t mode);

    返回:若成功则为新文件描述符,若出错为-1。

    open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件:O_RDONLY:只读、O_WRONLY:只写、O_RDWR:可读可写。flags参数也可以是一个或者更多位掩码的或,为写提供给一些额外的指示:O_CREAT:如果文件不存在,就创建它的一个截断的(truncated)(空)文件。O_TRUNC:如果文件已经存在,就截断它。O_APPEND:在每次写操作前,设置文件位置到文件的结尾处。

在这里插入图片描述

图 61 访问权限位

mode参数指定了新文件的访问权限位。作为上下文的一部分,每个进程都有一个umask,它是通过调用umask函数来设置的。当进程通过带某个mode参数的open函数调用来创建一个新文件时,文件的访问权限位被设置为mode&~umask。

  1. int close(int fd);

    返回:若成功则为0, 若出错则为-1 。

    进程通过调用 close 函数关闭一个打开的文件。关闭一个已关闭的描述符会出错。

  2. ssize_t read(int fd, void *buf, size_t n);

    返回:若成功则为读的字节数,若EOF则为0,若出错为-1。

    read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。

  3. ssize_t write(int fd, const void *buf, size_t n);

    返回:若成功则为写的字节数,若出错则为-1。

    write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。

  4. ssize_t rio_readn(int fd, void *usrbuf, size_t n);

    返回:若成功则为传送的宇节数,若EOF则为0(只对rio_readn而言),若出错则为-1。

    rio_readn函数从描述符fd的当前文件位置最多传送n个字节到内存位置usrbuf。

  5. int stat(const char *filename, struct stat *buf); int fstat(int fd, struct stat *buf);

    返回:若成功则为0,若出错则为-l。

    stat函数以一个文件名作为输入,一个stat数据结构中的各个成员。fstat函数是相似的,只不过是以文件描述符而不是文件名作为输入。

  6. DIR *opendir(const char *name);

    返回:若成功,则为处理的指针;若出错,则为NULL。

    函数opendir以路径名为参数,返回指向目录流(directorystream)的指针。流是对条目有序列表的抽象,在这里是指目录项的列表。

  7. struct dirent *readdir(DIR *dirp);

    返回:若成功,则为指向下一个目录项的指针;若没有更多的目录项或出错,则为NULL。

    每次对readdir的调用返回的都是指向流dirp中下一个目录项的指针,或者,如果没有更多目录项则返回NULL。

8.3printf的实现分析

1. printf函数

int printf(const char *fmt, …)

{

int i;

char buf[256];

va_list arg = (va_list)((char*)(&fmt) + 4);

i = vsprintf(buf, fmt, arg);

write(buf, i);

return i;

}

**分析:**这里我们可以看出(char*)(&fmt) + 4) 表示的是…中的第一个参数的地址。而之后printf函数调用了vsprintf函数,将参数传送给vsprintf,之后通过系统调用函数write进行输出。

  1. vsprintf函数

    在这里插入图片描述

图 62 vsprintf函数

**分析:**vsprintf返回的是要打印出来的字符串的长度。vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

  1. write函数

write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL

**分析:**write函数从内存位置buf复制至多n个字节到当前文件位置。

  1. sys_call

    sys_call:

    xor si,si

    mov ah,0Fh

    mov al,[ebx+si]

    cmp al,‘\0’

    je .end

    mov [gs:edi],ax

    inc si

    loop:

    sys_call

    .end:

    ret

    **分析:**write调用了sys_call,sys_call实现一个功能:显示格式化了的字符串。

之后字符显示驱动子程序:将ASCII映射到字模库而后向vram写入RGB有关信息(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4getchar的实现分析

int getchar(void)

{

char c;

return (read(0,&c,1)==1)?(unsigned char)c:EOF

//EOF定义在stdio.h文件中

}

在这里用户每在键盘上输入一个字符都会触发一个异步异常。异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。read函数从缓存区复制字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。这样就实现了读入功能。

8.5本章小结

本章介绍了Linux的IO设备的基本知识,并简述了Unix接口和相关的IO函数,最后分析了printf与getchar的实现,让我对第十章的内容理解的更加深刻。

结论

hello.c从一个.c文本文件到最后被CPU执行并回收进程让我看到了计算机软硬件系统的精密配合。正是这个庞杂且精密额的统支撑着计算机运行着各种各样的程序,为我们呈现精彩纷呈的数字世界。

让我们回顾hello的一生,最开始存放在磁盘的C文件hello,shell命令预处理器对其进行预处理,把头文件复制到程序的相应位置。之后shell又通过编译器,在新的进程中将其转化为汇编程序。在汇编器的帮助下,原本的程序中的逻辑地址变成了相对偏移,符号表与重定位条目建立起来。之后链接器通过动态链接,将其与库文件建立链接,并将相对偏移地址变成绝对偏移地址,链接函数使其在运行时可以动态的链接到库文件。在链接的过程中虚拟内存与进程管理向链接器伸出援手,免除其后顾之忧,它们通过私有内存与虚拟地址大大简化了链接器的工作。这样一个可执行文件在大家的帮助下就这样生成了。而它的旅途才刚刚开始。

Shell命令执行hello程序,计算机的各个部位都被调动起来。Shell使用fork函数创建了新的进程,exceve函数在加载器与内存管理单元的配合下将真实的物理地址加载到CPU中。CPU运行到动态链接相关函数,而此时不同的系统进程仍然在并发的执行。hello函数的第一条机器指令被加载到CPU中的一个线程中,在流水线机制的处理下,数十条指令正在执行。对puts函数的调用使系统输出函数运行起来,驱动程序将要显示的信息优雅的传送给相关硬件,IO忙碌起来。这时键盘传来了输入,键盘中断被触发,相应的处理程序将其传送给信息分析函数,一个SIGTSTP信号被发送出来,系统处理了这个信号,并将进程挂起。之后键盘又传来了输入,shell程序解析命令,向系统发出SIGCONT,进程被继续执行。不久后函数正常退出,父进程将其回收。它在虚拟内存与物理内存中的尸骸被清理。在短短几秒中hello结束了它的一生,只有原来的.c文件仍然留存,终端上的8行输出显示出它曾经来过。这就是hello的一生。

每当回望hello的一生,曾经的计算机工程师的智慧被我窥见一斑,而这仅仅是冰山一角,还有太多软件与硬件、系统与存储上的精妙设计还未被我们了解。相比于百年来无数人类智慧的结晶,那薄薄深入理解计算机系统只是为我打开一扇窗,使我得以在屋外瞥见那精妙的结构与设计,那百年的心血与伟力。

(结论0分,缺失-1分,根据内容酌情加分)

附件

文件名称说明对应本文章节
hello.ihello.c经预处理得到的文本文件第2章
hello.shello.i经编译得到的汇编代码文本文件第3章
hello.ohello.s经汇编得到的可重定位目标文件第4章
hello_elf.txthello.o经readelf分析得到的文本文件第4章
hellohello.o经链接得到的可执行文件第5章
hello1_elf.txthello经readelf分析得到的文本文件第5章

参考文献

[1]伍之昂LinuxShell编程从初学到精通(第2版)电子工业出版社,2015

[2]深入理解计算机系统(原书第三版).机械工业出版社, 2016.

[3][转]printf 函数实现的深入剖析 - Pianistx - 博客园 (cnblogs.com).

[4]read和write系统调用以及getchar的实现_Vincent’s Blog的博客-CSDN博客_getchar实现

[5] GCC online documentation. http://gcc.gnu.org/onlinedocs/
理内存中的尸骸被清理。在短短几秒中hello结束了它的一生,只有原来的.c文件仍然留存,终端上的8行输出显示出它曾经来过。这就是hello的一生。


附件

文件名称说明对应本文章节
hello.ihello.c经预处理得到的文本文件第2章
hello.shello.i经编译得到的汇编代码文本文件第3章
hello.ohello.s经汇编得到的可重定位目标文件第4章
hello_elf.txthello.o经readelf分析得到的文本文件第4章
hellohello.o经链接得到的可执行文件第5章
hello1_elf.txthello经readelf分析得到的文本文件第5章

参考文献

[1]伍之昂LinuxShell编程从初学到精通(第2版)电子工业出版社,2015

[2]深入理解计算机系统(原书第三版).机械工业出版社, 2016.

[3][转]printf 函数实现的深入剖析 - Pianistx - 博客园 (cnblogs.com).

[4]read和write系统调用以及getchar的实现_Vincent’s Blog的博客-CSDN博客_getchar实现

[5] GCC online documentation. http://gcc.gnu.org/onlinedocs/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值