计算机系统大作业HellosP2P

计算机系统

大作业

题 目 [程序人生-Hello’s P2P]{.underline}

专 业 [计算机科学与技术]{.underline}

学   号 [2022113329]{.underline}

班   级 [2203101]{.underline}

学 生 [***]{.underline}

指 导 教 师 [史先俊   ]{.underline}

计算机科学与技术学院

2023年4月

摘 要

论文介绍了hello P2P,
020的一生。P2P指的是由hello.c的源文件开始,编译系统经过预处理、编译、汇编、链接这些步骤得到可执行文件hello,再由shell为hello
fork进程并使用execve加载可执行文件,得到一个运行中的hello进程。

020指的是在执行hello前,内存里并没有其相关内容,这是第一个0. shell
fork进程后hello将在新的进程组中运行,execve将hello加载入内存,为其提供虚拟内存空间等进程上下文,hello便能够被机器执行。通过异常机制和信号,hello能够被内核调度,能够调用系统函数,能够通过中断与键盘进行交互等。最终,或者是终止信号,或者是程序自身退出,hello终止,操作系统结束hello进程,释放其占用的所有资源,hello又归于无,这是第二个0。

总而言之,只是输出一句简单的hello
world,背后却有那样多复杂的技术细节,这样多的底层机制互相合作,又得益于诸如虚拟内存,进程,unixIO,高级程序设计语言等等成功的抽象,使得计算机技术"飞入寻常百姓家",让hello
world成为无数程序员的第一条代码。惊叹于计算机系统的复杂与神奇的同时,我们也要更认真的学习相关知识,真正理解计算机系统的原理和精髓。

**关键词:**计算机系统;预处理;编译;汇编;链接;进程管理;内存管理

**
**

目 录

[第1章 概述 - 4 -](#第1章-概述)

[1.1 Hello简介 - 4 -](#hello简介)

[1.2 环境与工具 - 4 -](#环境与工具)

[1.3 中间结果 - 4 -](#_Toc532238399)

[1.4 本章小结 - 4 -](#源代码hello.c)

[第2章 预处理 - 5 -](#第2章-预处理)

[2.1 预处理的概念与作用 - 5
-
](#预处理的概念与作用)

[2.2在Ubuntu下预处理的命令 - 5
-
](#在ubuntu下预处理的命令)

[2.3 Hello的预处理结果解析 - 5
-
](#hello的预处理结果解析)

[2.4 本章小结 - 5 -](#本章小结-1)

[第3章 编译 - 6 -](#第3章-编译)

[3.1 编译的概念与作用 - 6 -](#编译的概念与作用)

[3.2 在Ubuntu下编译的命令 - 6
-
](#在ubuntu下编译的命令)

[3.3 Hello的编译结果解析 - 6
-
](#hello的编译结果解析)

[3.4 本章小结 - 6 -](#本章小结-2)

[第4章 汇编 - 7 -](#第4章-汇编)

[4.1 汇编的概念与作用 - 7 -](#汇编的概念与作用)

[4.2 在Ubuntu下汇编的命令 - 7
-
](#在ubuntu下汇编的命令)

[4.3 可重定位目标elf格式 - 7
-
](#可重定位目标elf格式)

[4.4 Hello.o的结果解析 - 7 -](#hello.o的结果解析)

[4.5 本章小结 - 7 -](#本章小结-3)

[第5章 链接 - 8 -](#第5章-链接)

[5.1 链接的概念与作用 - 8 -](#链接的概念与作用)

[5.2 在Ubuntu下链接的命令 - 8
-
](#在ubuntu下链接的命令)

[5.3 可执行目标文件hello的格式 - 8
-
](#可执行目标文件hello的格式)

[5.4 hello的虚拟地址空间 - 8
-
](#hello的虚拟地址空间)

[5.5 链接的重定位过程分析 - 8
-
](#链接的重定位过程分析)

[5.6 hello的执行流程 - 8 -](#hello的执行流程)

[5.7 Hello的动态链接分析 - 8
-
](#hello的动态链接分析)

[5.8 本章小结 - 9 -](#本章小结-4)

[第6章 hello进程管理 - 10
-
](#第6章-hello进程管理)

[6.1 进程的概念与作用 - 10 -](#进程的概念与作用)

[6.2 简述壳Shell-bash的作用与处理流程 - 10
-
](#简述壳shell-bash的作用与处理流程)

[6.3 Hello的fork进程创建过程 - 10
-
](#hello的fork进程创建过程)

[6.4 Hello的execve过程 - 10 -](#hello的execve过程)

[6.5 Hello的进程执行 - 10 -](#_Toc532238431)

[6.6 hello的异常与信号处理 - 10
-
](#hello的异常与信号处理)

[6.7本章小结 - 10 -](#本章小结-5)

[第7章 hello的存储管理 - 11
-
](#第7章-hello的存储管理)

[7.1 hello的存储器地址空间 - 11
-
](#hello的存储器地址空间)

[7.2 Intel逻辑地址到线性地址的变换-段式管理 - 11
-
](#intel逻辑地址到线性地址的变换-段式管理)

[7.3 Hello的线性地址到物理地址的变换-页式管理 - 11
-
](#hello的线性地址到物理地址的变换-页式管理)

[7.4 TLB与四级页表支持下的VA到PA的变换 - 11
-
](#tlb与四级页表支持下的va到pa的变换)

[7.5 三级Cache支持下的物理内存访问 - 11
-
](#三级cache支持下的物理内存访问)

[7.6 hello进程fork时的内存映射 - 11
-
](#hello进程fork时的内存映射)

[7.7 hello进程execve时的内存映射 - 11
-
](#hello进程execve时的内存映射)

[7.8 缺页故障与缺页中断处理 - 11
-
](#缺页故障与缺页中断处理)

[7.9动态存储分配管理 - 11 -](#动态存储分配管理)

[7.10本章小结 - 12 -](#本章小结-6)

[第8章 hello的IO管理 - 13
-
](#第8章-hello的io管理)

[8.1 Linux的IO设备管理方法 - 13
-
](#linux的io设备管理方法)

[8.2 简述Unix IO接口及其函数 - 13
-
](#简述unix-io接口及其函数)

[8.3 printf的实现分析 - 13 -](#printf的实现分析)

[8.4 getchar的实现分析 - 13 -](#getchar的实现分析)

[8.5本章小结 - 13 -](#本章小结-7)

[结论 - 14 -](#结论)

[附件 - 15 -](#附件)

[参考文献 - 16 -](#参考文献)

第1章 概述

1.1 Hello简介

Hello的P2P(From Program to
Process):由hello.c的源文件开始,编译系统经过预处理、编译、汇编、链接这些步骤得到可执行文件hello,再由shell为hello
fork进程并使用execve加载可执行文件,得到一个运行中的hello进程。

Hello的020(From Zero to
Zero):在执行hello前,内存里并没有其相关内容,这是第一个0. shell
fork进程后hello将在新的进程组中运行,execve将hello加载入内存,为其提供虚拟内存空间等进程上下文,hello便能够被机器执行。通过异常机制和信号,hello能够被内核调度,能够调用系统函数,能够通过中断与键盘进行交互等。最终,或者是终止信号,或者是程序自身退出,hello终止,操作系统结束hello进程,释放其占用的所有资源,hello又归于无,这是第二个0。

1.2 环境与工具

[]{#_Toc532238399 .anchor}硬件环境

AMD Ryzen 7 6800X X64 CPU; 3.2GHz; 16G RAM; 256GHD Disk以上

软件环境

Windows11 64位

开发工具

VM VirtualBox 6.1;Ubuntu 20.04 LTS 64位;

Visual Studio 2019 64位;CodeBlocks 17.12 64位;vi/vim/gedit+gcc。

1.3 中间结果

1. 源代码hello.c

2. 经过预处理器生成的修改了的源文件hello.i

3. 经过编译器生成的汇编语言文件hello.s

4. 经过汇编器生成的可重定位目标文件hello.o

5. 经过链接器生成的可执行目标文件hello

6. 链接器生成的过程verbose.txt

7. hello.o通过objdump得到的反汇编文件asmhello.txt

8. hello通过objdump得到的反汇编文件asmhello2.txt

1.4 本章小结

本章主要介绍了hello.c程序P2P,020的过程,列出了本次实验所需的环境和工具以及过程中所生成的中间结果。

第2章 预处理

2.1 预处理的概念与作用

预处理器根据以字符#开头的命令修改原始的C程序, 包括:
插入所有用#include命令指定的文件, 并扩展所有用#define声明指定的宏.
经过预处理器, 可以得到.i文件, i意为intermediate.

作用:

1. 包含其他文件, 一般的说是头文件, 如stdio.h. 预处理器将#include
<stdio.h>替换为文件"stdio.h"的文本内容. 经常使用include
guards和#pragma once来避免多次include, 造成程序体积膨胀.

2. 条件编译: 通过#if, #ifdef, #ifndef, #else, #elif, #endif
等语句可以进行条件编译. 有助于我们方便地编写跨平台, 跨处理器的代码,
也便于使用一些仅用于Debug阶段的代码.

3. 宏定义. 有两种宏: 对象宏(object-like macro)和函数宏(function-like
macro). 对象宏允许我们定义一些常量(const), 避免幻数, 提高程序的可读性,
缺陷是宏定义无法指定对象的类型. 函数宏可以就地扩展成代码,
可以替代一些简单的函数同时避免调用的开销.
缺点在于大量函数宏的扩展同样会造成程序体积的膨胀,
同时由于函数宏只是简单地做扩展, 可能造成语句的语义不清晰, 产生bug.

函数宏还能够增强C语言的表达能力, 实现一定程度的元编程.

4. 特殊宏和指令. 预处理器定义了一些宏比如__FILE__, __LINE__,
可以方便我们调试, 使得源代码可以访问一些只有到编译阶段才能知道的信息.
使用#line可以修改__FILE__, __LINE__的值.

5. 删去注释内容.

6. 使用#error通过错误流输出信息.

7. 可以使用一些预处理器功能, 它们适用于特定的编译器或者语言,
比如使用#pragma once, #warning "xxx"等.

2.2在Ubuntu下预处理的命令

gcc -E hello.c -o hello.i

下图使用了–verbose

2.3 Hello的预处理结果解析

产生了.i文件, 这里是hello.i.

如图,可见预处理后main函数本身的代码只占hello.i的很小一部分,绝大部分是展开的头文件和其他内容。

2.4 本章小结

本章介绍了预处理的概念和作用,
预处理器根据以字符#开头的命令修改原始的C程序, 生成.i文件. 通过具体例子,
对Ubuntu下预处理的命令和结果进行简要分析.

第3章 编译

3.1 编译的概念与作用

编译器(cc1)将文本文件hello.i翻译成文本文件hello.s,
它包含一个汇编语言程序.

脱离C的语境, 编译是这样的过程: 把源程序映射为在语义上等价的目标程序,
或者具体一些,
将用一种编程语言(源语言)编写的计算机代码翻译成另一种语言(目标语言),
主要是将源代码转换成低级编程语言(比如汇编语言)以便于创建可执行程序.

可以将编译的过程简要分成两个部分: 分析部分和综合部分.
分析(analysis)部分把源程序分解为多个组成要素,
并在这些要素之上加上语法结构,
然后使用这个结构来创建该源程序的一个中间表示.
分析部分还会收集有关源程序的信息, 并把信息存放在一个称为符号表(symbol
table)的数据结构中, 符号表将和中间表示形式一起传送给综合部分.

综合(synthesis)部分根据中间表示和符号表中的信息来构造用户期待的目标程序.
分析部分经常被称为编译器的前端(front end), 而综合部分称为后端(back end)

(以上内容摘自编译原理)

作用: 把经过预处理的源程序翻译成目标汇编程序,
方便汇编器生成可重定位机器代码。

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s -fno-PIC -no-pie -m64

下图使用了–verbose

3.3 Hello的编译结果解析

1. 数据:

– 字符串: 存储在.rodata段里, 即只读数据段.

– 立即数: 在汇编语句中直接出现.

– 局部变量: 存储在寄存器或栈中.
本例中函数main中的局部变量i存储在栈中地址为%rbp-4的位置处.

– 参数: 编译器分别使用%rdi, %rsi, %rdx, %rcx, %r8,
%r9作为函数的第1,2y…6个参数, 如果参数数量比6还要多, 就用栈来传递.
本例中main函数的两个参数argc和argv分别存储在%edi和%rsi上,
要调用函数传参时, 就把%edi, %rsi存到栈里

2. 赋值 汇编语言通过mov指令来实现赋值操作. 本例中, 使用movl语句给i赋0.

3. 算术操作 汇编语言有多种指令来实现算术操作, 比如add, sub, mul,
比较指令cmp也要进行算术运算. 对于复杂的算术操作, 编译器会用多条汇编语言,
移位操作以及lea借助寻址方式简化运算.

本例中for循环中局部变量的自增操作i++在汇编语言中是通过addl实现的.

4. 关系操作 汇编语言通过cmp和其他指令组合完成关系操作.
本质上cmp是进行一次减法运算, 但是扔掉运算的结果;
后续的指令通过运算过程中cpu设置的flag来判断满不满足某个关系.

本例中判断argc != 4, i < 8都用到了关系判断. 比如i < 8,
使用cmpl和jle组合完成了关系操作.

5. 数组/指针操作 对于汇编语言来说, 数组操作本质上就是指针操作.

本例中使用了char* argv[], 实际上等同于char** argv.
汇编语言先将argv的值, 这里在栈中%rbp-32处, 存储到%rax, 然后添加偏移量,
由于linux x64下一个指针占8个字节, 把argv + 16byte存到%rdx, 即第3个参数;
把argv + 8b 存到%rsi, 即第2个参数. 由于这里的运算很简单,
编译器没有使用寻址方法.

6. 控制转移 一般控制转移和关系操作是一并出现的.

– if判断: 可见首先使用cmpl指令, 等同于计算argc-4 然后使用je指令,
je指令看cpu的ZF(zero flag)是否设置, 如果ZF=1, 则跳至.L2处, 否则不跳转,
等同于argc!=4, 执行 大括号内部的语句.

– for 循环: 在进入循环体之前, 程序先跳至.L3处, 首先使用cmpl指令,
等同于计算i-7, 再使用jle指令, 如果ZF=1(相等) or SF!=OF(i-7<0,i<7),
那么进入循环体, 否则跳出循环. 在循环体中, 最后会执行i++的语句.

7. 函数操作

– 参数传递 编译器分别使用%rdi, %rsi, %rdx, %rcx, %r8,
%r9作为函数的第1,2y…6个参数, 如果参数数量比6还要多, 就用栈来传递.
本例中main函数的两个参数argc和argv分别存储在%edi和%rsi上, 要调用函数时,
如果要用到这些寄存器, 那么就把它们保存到栈中, 再存入参数的值.

– 函数调用 使用call来调用函数.

– 局部变量 如果程序用的局部变量很少, 可能使用寄存器就能解决问题;
否则就由被调的函数申请合适的栈空间存放这些变量.
本例中main函数的局部变量i就存在栈中%rbp-4的位置.

– 函数返回 调用者函数里使用%rax就能使用最近被调函数的返回值.
被调函数需要把合适的返回值存在%rax中, 然后使用leave, ret
语句完成释放栈空间, 返回调用者函数。

3.4 本章小结

本章介绍了hello.i文件编译成hello.s文件的过程,
了解了C语言中的数据和操作是怎么用汇编语言存储和实现的,
通过具体例子加深了对这一过程的理解.
通过理解这种机制我们可以增强对C语言底层机制的理解,
方便我们通过查看汇编语言了解程序错误的原因和制约性能的因素。

第4章 汇编

4.1 汇编的概念与作用

汇编器(as)将hello.s翻译成机器语言指令,
把这些指令打包成一种叫做可重定位目标程序(relocatable object
program)的格式, 并将结果保存在目标文件hello.o中.

作用:

• 汇编过程将汇编语言转换成机器能够执行的二进制机器语言指令,
使得程序可以被执行.

注意:这儿的汇编是指从 .s 到 .o
即编译后的文件到生成机器语言二进制程序的过程。

4.2 在Ubuntu下汇编的命令

gcc -c hello.s -o hello.o -fno-PIC -no-pie -m64

同样使用了–verbose

4.3 可重定位目标elf格式

ELF可重定位目标文件的格式如下:

可以使用readelf来查看ELF的内容

• ELF头(ELF Header)

使用readelf -h hello.o查看ELF头内容如下

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

• 节头部表(Section header table)

使用readelf -S hello.o查看节头部表内容如下.

节头部表描述了不同节的位置和大小,
目标文件中的每个节都有一个固定大小的条目(entry)

• 节(sections)

使用 readelf -a hello.o可以列出ELF的所有信息.

下面查看了一些节的信息。

.rela.text是一个 .text 节中位置的列表,
当链接器把这个目标文件和其他文件组合时, 需要修改这些位置.
一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非用户显式地指示链接器包含这些信息。

.rela.data是被模块引用或定义的所有全局变量的重定位信息, hello.c
没有用到全局变量, 所以没有这个节.

.rela.eh_frame包含了.eh_frame节的重定位信息

.eh_frame节可以用于处理异常, 它生成描述如何展开栈的表,
这在支持异常处理的语言中是必要的, 比如C++. 虽然C没有现代的异常处理机制,
但是gcc仍会生成这一节.

.text节存储已编译程序的机器代码.

.strtab:一个字符串表,其内容包括 .symtab 和 .debug
节中的符号表,以及节头部中的节名字。字符串表就是以 null
结尾的字符串的序列。

.symtab:
一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。每个可重定位目标文件在
.symtab 中都有一张符号表(除非程序员特意用 STRIP
命令去掉它)。然而,和编译器中的符号表不同,.symtab
符号表不包含局部变量的条目。

4.4 Hello.o的结果解析

机器语言是由01构成的二进制代码表示的计算机能直接识别和执行的一种机器指令系统指令的集合,机器指令由操作码,操作数地址和操作结果的存储地址三部分组成。而每一条汇编语言都可以用机器指令表示。

使用命令objdump -s -d hello.o可以反汇编hello.o.

不同点: 反汇编的代码中, .L1, .L2等label被替代了.

• 分支跳转: 反汇编的代码中, 分支跳转使用的是相对地址.

• 函数调用: 在hello.s中, 函数调用时call函数名, 而反汇编文件中是地址.

• 访问字符串常量: 和跳转指令一样, 反汇编使用的是地址.

4.5 本章小结

本章主要介绍了从hello.s到hello.o的汇编过程,
查看分析了hello.o可重定位目标文件的ELF格式代码,
使用了objdump查看了反汇编生成的代码并与hello.s进行对比。

第5章 链接

5.1 链接的概念与作用

链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时(compile
time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load
time),也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(runtime),也就是由应用程序来执行。在早期的计算机系统中,链接是手动执行的。在现代系统中,链接是由叫做链接器(linker)的程序自动执行的。

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

5.2 在Ubuntu下链接的命令

ld --verbose -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

过程见附verbose.txt 文件

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

• ELF头

类似上一章的分析, 区别在于类型变为了EXEC, 入口点地址也变为0x401090.

• 节头部表

可以看到链接器合并了多个可重定位目标文件的段,
各个段的大小和段的数量都有变化. 可见.text段的起始地址就是401090,
和入口点地址相同.

• 重定位节

可以看到现在重定位节为.rela.dyn和.rela.plt.

• 符号表

hello的符号表分成了两部分


.dynsym中的是需要用到的在共享库中符号的信息,这些符号表条目在动态链接时会发挥作用。

• .symtab中包含的是已经被重定位的符号.
可以看到在.dynsym中出现的项Value为0, Ndx为UND, 这是由于它们是动态链接的.

5.4 hello的虚拟地址空间

各段空间如图所示. 虚拟内存的起始地址默认是0x400000,
0x401000是.init节的起点, 0x402000是.rodata节的起点,
0x403000到0x405000存储着其他一些节.

5.5 链接的重定位过程分析

objdump的结果存储在asmhello2.txt中.

不同:

• 地址不同: .text的起始地址, 或者入口点地址变为0x401090.
0x401090存放的是_start的机器码, main的起始地址变为0x4010c5.

• 新的节: hello增加了如.init, .plt等节.

• call参数不同: 在hello.o的反汇编里, call 的参数均为下一条指令的地址;
在hello的反汇编中, call的参数是.plt节中的地址.

重定位过程:

重定位由两步构成:

• 重定位节和符号定义. 在这一步中, 链接器将所有相同类型的节合并为
同一类型的新的聚合节. 然后, 链接器将运行时内存地址赋给新的聚合节,
赋给输入模块定义的每个节, 以及赋给输入模块定义的每个符号.
当这一步完成时, 程序中的每条指令和全局变量都有唯一的运行时内存地址了.

• 重定位节中的符号引用. 在这一步中, 链接器修改代码节和数据节中对
每个符号的引用, 使得它们指向正确的运行时地址. 要执行这一步,
链接器依赖于可重定位目标模块中称为重定位条目的数据结构.
代码的重定位条目放在.rel.text中,
已初始化数据的重定位条目放在.rel.data中.
这些重定位条目告诉链接器对这些符号的引用要用某种方法进行重定位.

对于标准库函数, 采用动态链接共享库的方式来链接, 使用延迟绑定技术,
通过PLT和GOT协作可以在运行时解析函数的地址.
可以在.plt节里看到这些函数的条目.

5.6 hello的执行流程

加载Hello后, 第一个调用的程序是_dl_start, 地址是0x7ffca8858540.

接着调用的程序是_dl_init, 地址是0x7fb2578e32b0, 执行一系列初始 化过程.

然后跳至_start, 地址为0x00401090.

之后调用__libc_start_main_impl, 地址为0x7febbdbb1180.

再调用__cxa_atexit,地址为0x7f8321d20960.

还要调用__internal_atexit, __new_exitfn, csu_init _setjmp,
__sigsetjmp等一系列函数.

最后终于进入main函数. 地址为0x004010c5.

5.7 Hello的动态链接分析

hello的动态链接用到了延迟绑定的策略. 使用延迟绑定的动机是对于标准库
这样很大的库, 一个典型的应用程序只会使用其中很少的一部分, 把函数地址
的解析推迟到它实际被调用的地方, 能避免动态加载器在加载时进行成百上千个
其实并不需要的重定位. 第一次调用过程的运行时开销很大, 但是之后的每次
调用都只会花费一条指令和一个间接的内存引用.

延迟绑定是通过GOT和PLT之间的交互来实现的. GOT是数据段的一部分,
而PLT是代码段的一部分.

.got, .got.plt的节如图所示. 由于hello中的 库函数只调用了一次,
每次都会跳转至动态链接器中, 所以.got节只分配了16字节, 也就是只有两项.

下面在edb里进行探究.

调用_dl_start之前

调用_dl_plt之后

可以看到_dl_start函数完成了初始化.got, .got.plt两节的任务.

这是_dl_init前后.got, .got.plt节的内容, 似乎没有发生变化.

5.8 本章小结

本章我们详细分析了链接的过程,比较了hello.txt和hello.o.txt之间的不同,分析了hello程序的执行流程,包括动态链接是如何进行的,体会了链接的复杂和强大,使我们能够高效地编写大型程序并节省编译时间和内存。

第6章 hello进程管理

6.1 进程的概念与作用

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

作用:进程是计算机科学中最深刻、最成功的概念之一。在现代系统上运行一个程序时,我们会得到一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象都是通过进程的概念提供给我们的。

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

Shell的作用:

shell 执行一系列的读/求值(read/evaluate)步骤,然后终止。
读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。

处理流程

1. Shell等待一个从终端输入的命令

2. 解析命令行参数

3. 判断是否是内置命令

4. 如果是内置命令,立即执行

5. 否则调用fork函数创建一个子进程

6. 再调用execve函数将程序加载到子进程里运行

6.3 Hello的fork进程创建过程

当shell接收到命令行时, 它会按上面的流程进行解析, 本例中它发现./hello
不是一个内置命令, 于是调用fork函数创建一个新进程.

父进程通过调用fork函数创建一个新的运行的子进程。调用fork函数后,新创建的子进程几乎但不完全与父进程相同:子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本(包括代码、数据段、堆、共享库以及用户栈),子进程获得与父进程任何打开文件描述符相同的副本,这意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。fork被调用一次,却返回两次,子进程返回0,父进程返回子进程的PID。子进程有不同于父进程的PID。shell还会给子进程分配新的PGID,
这和信号处理有关系.

6.4 Hello的execve过程

[]{#_Toc532238431 .anchor}fork之后,
shell在子进程中调用execve函数来加载可执行文件到当前 进程.
execve函数接收3个参数, 分别为filename, argv和envp.
只有发生错误时execve才会返回调用程序, 因此execve调用一次且从不 返回.

execve会执行以下几个步骤:

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

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

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

4. 设置程序计数器(PC)。execve
做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

6.5 Hello的进程执行

进程是轮流使用处理器的, 每个进程执行它的流的一部分,
然后被抢占(preempted)(暂时挂起), 然后轮到其他进程.
一个进程执行它的控制流的一部分的每一时间段叫做时间片.

内核为每个进程维持一个上下文.
上下文就是内核重新启动一个被抢占的进程所需的状态. 在进程执行的某些时刻,
内核可以决定抢占当前进程, 并重新开始 一个先前被抢占了的进程,
这种决策就叫做调度, 是由内核中称为调度器的代码 处理的.
当内核调度了一个新的进程运行后, 它就抢占当前进程, 并使用一种
称为上下文切换的机制来将控制转移到新的进程.
上下文切换1)保存当前进程的上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新恢复的进程。

% 图

对于hello程序, hello开始运行时, 内核保存它的上下文, hello在用
户模式下正常执行, 直到产生异常或者系统中断, 这时候处理器设置模式位,
切换到内核模式, 如果产生异常, 控制传递到异常处理程序, 处理程序运行在
内核模式中, 返回到应用程序时内核模式会改回到用户模式.

当内核代表用户执行系统调用时, 可能会发生上下文切换.
如果系统调用因为等待某个事件发生而阻塞, 那么内核可以让当前进程休眠,
切换到另一个进程. 本例中getchar系统调用等待用户击键,
内核可能选择执行上下文切换, 运行另外一个进程;
sleep系统调用显式地请求让调用进程休眠. 一般而言, 即使系统调用没有阻塞,
内核也可以决定执行上下文切换, 而不是将控制返回给 调用进程.

中断也可能引发上下文切换. 所有的系统都有某种产生周期性定时器中断的机制,
通常为每1毫秒或每10毫秒. 每次发生定时器中断时,
内核就能判定当前进程已经运行了足够长的时间, 并切换到一个新的进程.

6.6 hello的异常与信号处理

hello执行过程中出现的异常:

1.
中断:来自I/O设备的信号,异步发生,中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码;比如:
定时器中断, 键盘.

2.
陷阱:有意的异常,是执行一条指令的结果,调度后也会返回到下一条指令,用来调度内核服务进行操作,帮助程序从用户模式切换到内核模式;比如:
getchar.

3.
故障:由错误引起,它可能被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则终止程序;

4.
终止:不可恢复的致命错误造成的结果,处理程序会将控制返回到一个abort例程,该例程会终止这个应用程序。

可能出现的信号:

对各种信号的分析:

1. 执行中乱按键盘

字符和回车可以正常输出在屏幕上, 不影响程序运行.
分析认为这里按下按键会产生一个键盘中断, 切换到内核模式处理中断,
结果是将对应字符输出到屏幕上再切换回用户模式继续执行hello.

由于调用了getchar, 第一次回车前的缓冲区没有被输出到命令行中,
其余的还在缓冲区里, 在执行完毕后都输出到命令行里了.

2. Ctrl-Z

按下Ctrl-Z会产生键盘中断, 然后切换到内核模式处理中断,
发送SIGTSTP信号到shell,
shell再把信号转发到当前前台作业hello对应的进程组, 现在是默认情况,
结果是停止(挂起)当前作业.

按下Ctrl-Z后, hello被停止, 但是进程没有被杀死,
用ps和jobs可以看到进程和任务hello还存在.
使用fg会将停止的任务hello切换回前台执行.
使用kill命令发送SIGKILL信号后程序终止, 进程被杀死,
使用ps可见hello的进程已杀死.

3. Ctrl-C

同理, 内核向shell发送SIGINT信号,
shell把信号转发到当前前台作业即hello对应的进程组,
收到信号后执行默认行为终止进程.

6.7本章小结

本章主要介绍了进程的概念和作用,阐述了shell的作用和处理流程。重点描述了hello进程调用fork函数创建子进程和execve函数执行进程的过程。最后分析了hello程序执行过程中可能会出现的异常以及对这些异常的处理。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:这是Intel在分段内存模型中使用的一个术语,
在分段模型中内存被划分为段(segments), 要对段中的内容进行寻址,
需要用到逻辑地址. 逻辑地址由一个段选择器和一个偏移量组成.

线性地址: CSAPP是这样写的, 概念上讲, 如果地址空间中的证书是连续的,
我们就说它是一个线性地址空间. 如果不使用内存分段,
那么虚拟内存空间也是线性地址空间, 虚拟地址也是线性地址,反之不是。

虚拟地址: 虚拟地址是虚拟内存空间内的地址,
它对应着虚拟内存空间里的代码或数据, 在64位系统上表示为一个64位整数.

物理地址:是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。在hello的运行中,在访问内存时需要通过CPU产生虚拟地址,然后通过地址翻译得到一个物理地址,并通过物理地址访问内存中的位置。

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

逻辑地址是Intel在分段内存模型中使用的一个术语. 要对段中的内容进行寻址,
需要用到逻辑地址. 它由一个段标识符(segment
selector)和一个偏移量(offset)组成。
其中段标识符是一个16位长的字段,称为段选择符,前13位是一个索引号,后面3位包含一些硬件细节。

索引号,类似数组下标,它对应的"数组"就是段描述符表,段描述符具体描述了一个段地址,多个段描述符组成段描述符表。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。其中Base字段描述了一个段的开始位置的线性地址。

全局段描述符:放在全局段描述符表(GDT中),局部段描述符:放在局部段描述符表(LDT中)

给定一个完整的逻辑地址 [段选择符:段内偏移地址]
,段选择符T1等于0或1,分别转换到GDT或LDT中的段,再根据相应寄存器,得到其地址和大小,就有了一个数组了。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,base +
offset

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

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

linux使用页式管理, 在linux下, 逻辑地址同线性地址是等同的.

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

在任意时刻,虚拟页面的集合都分为三个不相交的子集:未分配的:VM
系统还未分配(或者创建)的页。未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间。缓存的:当前已缓存在物理内存中的已分配页。未缓存的:未缓存在物理内存中的已分配页。

CPU中的一个控制寄存器, 页表基址寄存器(PTBR)指向当前页表.
虚拟地址包含两部分: 虚拟页面偏移(VPO)和虚拟页号(VPN),
内存管理单元MMU利用VPN来选择合适的PTE,
将页表条目中物理页号(PPN)和虚拟地址里的VPO串联起来,
就得到相应的物理地址. PPO和VPO是相同的。

当页面命中时, CPU硬件会执行下列步骤:

第 1 步:处理器生成一个虚拟地址,并把它传送给 MMU.

第 2 步:MMU 生成 PTE 地址(PTEA),并从高速缓存/主存请求得到它。

第 3 步:高速缓存/主存向 MMU 返回 PTE.

第 4 步:MMU 构造物理地址,并把它传送给高速缓存/主存。

第 5 步:高速缓存/主存返回所请求的数据字给处理器。

页面不命中时情况复杂一些, 处理缺页要求硬件和操作系统内核协作完成:

第1-3步: 相同。

第4步: PTE的有效位是0,所以MMU触发异常,
传递控制到操作系统内核中的缺页异常处理程序。

第5步: 缺页处理程序确定出物理内存中的牺牲页, 如果这个页面已经被修改 了,
则把它换出到磁盘。

第6步:缺页处理程序调入新的页面,并更新内存中的PTE.

第7步: 缺页处理程序返回到原来的进程, 再次执行导致缺页的指令. CPU
将引起缺页的虚拟地址重新发送给MMU.下面和页面命中的情况就一样了。

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

为了消除查阅PTE的开销, 许多系统在MMU中包括了一个关于PTE的小的缓存,
称为后备缓冲器(Translation Lookaside Buffer, TLB).
TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个 PTE
组成的块。 如果 TLB 有 T=2^t 个组,那么 TLB 索引(TLBI)是由 VPN 的 t
个最低位组成的, 而 TLB 标记(TLBT)是由 VPN 中剩余的位组成的。

当TLB命中时, 执行以下步骤:

第 1 步:CPU 产生一个虚拟地址。

第 2 步和第 3 步:MMU 从 TLB 中取出相应的 PTE。

第 4 步:MMU
将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存。

第 5 步:高速缓存/主存将所请求的数据字返回给 CPU。

当TLB不命中时, MMU还要从L1缓存中取出相应的PTE, 把新取出的PTE
存放在TLB中, 这可能会覆盖一个已经存在的条目.

由于虚拟内存区域相当巨大, 只用一个页表会浪费相当多的内存,
使用多级页表可以减少这种浪费. 实践中可能会使用四级页表.

下图给出了Intel Core i7 MMU如何使用四级页表进行地址翻译. 36 位 VPN
被划分成四个 9 位的片,每个片被用作到一个页表的偏移量。CR3 寄存器包含 L1
页表的物理地址。VPN 1 提供到一个 L1 PTE 的偏移量,这个 PTE 包含 L2
页表的基地址。VPN 2 提供到一个 L2 PTE 的偏移量,以此类推。
最后在四级页表中提供了物理页号PPN,
由前面的知识可知和VPO串联起来就得到了物理地址PA.

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

经过以上步骤, MMU得到了物理地址PA, 接着将物理地址传送到L1高速缓存,
如果命中, 即在L1 缓存里有对应内存块, 则访问L1 Cache.
这时同样的虚拟地址被解析为另外三个部分: CT, CI和CO (CT: Cache Tag, CO:
Ca Offset, CI: Ca Index). 根据CI可以确定在哪个组, 根据CT可以确定到行,
如果有标记位匹配而且这一行的有效位被设置, 那么缓存命中, 通过CO可以
访问目标, 否则缓存未命中, 在L2, L3重复上述步骤, 如果都未命中,
只好到物理内存中寻找. 如果 物理内存里有这个页, 则访问它,
不幸的话物理内存里没有这一页产生缺页中断, 调用缺页异常处理程序
如果物理内存已满还要选择一个牺牲页, 被修改还要把它写回磁盘,
再把磁盘里的页复制到物理内存, 更新页表,
异常处理程序返回并重新启动导致缺页的指令.

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序替代当前程序。具体过程如下:

1. 删除已存在的用户区域,删除当前进程虚拟地址的用户部分已存在的结构;

2.
映射私有区域,为新程序的代码、数据、bss和堆栈区域创建新的区域结构,这些区域都是私有、写时复制的。代码和数据区域被映射为hello文件的.text节和.data节,bss区域是请求二进制0的,映射到匿名文件,堆栈区域也是请求二进制0的,初始长度为0;

3.
映射共享区域:hello程序与共享对象libc.so链接,libc.so是动态链接的,映射到用户虚拟地址空间中的共享区域内;

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

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

如果指令引用的虚拟地址, 与其对应的物理页面不在内存中,
就会产生一个缺页中断, 此时控制转移给缺页处理程序,
处理程序随后就执行下面的步骤:

1. 虚拟地址 A 是合法的吗?换句话说,A
在某个区域结构定义的区域内吗?为了回答这个问题,缺页处理程序搜索区域结构的链表,把
A 和每个区域结构中的 vm_start 和 vm_end
做比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。这个情况在图
9-28 中标识为 “1”。 因为一个进程可以创建任意数量的新虚拟内存区域(使用
mmap
函数),所以顺序搜索区域结构的链表花销可能会很大。因此在实际中,Linux
使用某些我们没有显示出来的字段,Linux
在链表中构建了一棵树,并在这棵树上进行查找。

2.
试图进行的内存访问是否合法?换句话说,进程是否有读、写或者执行这个区域内页面的权限?例如,这个缺页是不是由一条试图对这个代码段里的只读页面进行写操作的存储指令造成的?这个缺页是不是因为一个运行在用户模式中的进程试图从内核虚拟内存中读取字造成的?如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。这种情况在图
9-28 中标识为 “2”。

3.
此刻,内核知道了这个缺页是由于对合法的虚拟地址进行合法的操作造成的。它是这样来处理这个缺页的:选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU
重新启动引起缺页的指令,这条指令将再次发送 A 到 MMU。这次,MMU
就能正常地翻译 A,而不会再产生缺页中断了。

7.9动态存储分配管理

1. 基本原理:

在程序运行时程序员使用动态内存分配器获得虚拟内存。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。已分配的块显式的保留供应用程序使用。空闲块保持空闲,直到显式的被应用所分配。一个已分配的块保持已分配状态,直到其被释放。

2. 类型:

1. 显式分配器:要求应用显式的释放任何已分配的块;

2. 隐式分配器:应用检测到已分配块不再被程序所使用,就释放这个块。

3. 常用的策略和方法有四种:

1.
首次适应:首次适应策略要求空闲区按其起始地址从小到大排列,当某一用户作业要求装入内存时,存储分配程序从起始地址最小的空间区开始扫描,直到找到满足该作业要求的空闲区为止。

2.
循环首次适应:在查找空闲区时,不再每次从链首开始查找,而是从上一次找到的空闲区的下一个空闲区开始查找,直到找到一个能满足要求的空闲区为止,并从中划出一块与请求大小相等的内存空间分给该作业。

3.
最佳适应:该策略总是把满足要求,又使最小的空闲区分配给请求作业,即在空闲区表中,按空闲区的大小从小到大排列,建立索引,当用户作业请求内存空间时,从索引表中找到第一个满足该作业的空闲区分给它。

4.
最差适应:该策略总是把最大的空闲区分配给请求作业,空闲区表(空闲区链)中的空闲分区要按大小从大到小进行排序,自表头开始查找到第一个满足要求的空闲分区分配给作业。

7.10本章小结

本章详细分析了hello程序看似简单的内存访问背后的复杂机制,包括逻辑地址到虚拟地址的转换、基于分页机制的虚拟地址到物理地址的转换、内存与三级Cache组成的层次结构以及缺页故障和缺页中断处理等机制。这其中的虚拟内存机制在访问内存操作中起着极为重要的作用,需要我们认真学习并掌握。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件。所有的I/O设备都被模型化为文件,所有的输入和输出都被当做相应文件的读和写完成。

设备管理:unix
io接口。上面这种将设备优雅的映射成文件的方式,允许Linux内核引出一个简单的,低级的应用接口,称为Unix
I/O,这使得所有的输入输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

Unix I/O接口:

1.打开文件:程序要求内核打开文件,内核返回一个小的非负整数(描述符),用于标识这个文件。程序只要记录这个描述符便能记录打开文件的所有信息;

2.shell在进程的开始为其打开三个文件:标准输入、标准输出和标准错误;

3.改变当前文件的位置:对于每个打开的文件,内核保存着一个文件位置k,初始为0.这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作显式地设置文件的当前位置为k;

4.读写文件:一个读操作就是从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会触发一个称为EOF的条件,应用程序能够检测到这个条件,在文件结尾处没有明确的EOF符号;

5.关闭文件:内核释放打开文件时创建的数据结构以及其占用的内存资源,并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因停止时,内核都会关闭所有打开的文件并释放他们的内存资源。

Unix IO函数:

1.打开文件:int open(char filename, int flags, mode_t mode);
Open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程当中没有打开的最小描述符。Flags参数指明了进程打算如何访问这个文件,同时也可以是一个或者更多为掩码的或,为写提供给一些额外的指示。Mode参数指定了新文件的访问权限位。

2.关闭文件:int close(int fd);
调用close函数,通知内核结束访问一个文件,关闭打开的一个文件。成功返回0,出错返回-1。

3.读文件:ssize_t read(int fd, voidbuf, size_t n);
调用read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示错误,返回值0表示EOF,否则返回值表示的是实际传送的字节数量。

4.写文件:ssize_t write(int fd, const void *buf, size_t n);
调用从内存位置buf复制至多n个字节到描述符fd的当前文件位置。返回值-1表示出错,否则,返回值表示内存向文件fd输出的字节的数量。

8.3 printf的实现分析

printf函数:

int printf(const char *fmt, ...)
{
    int i;
    va_list arg = (va_list)((char *)(&fmt) + 4);
    i = vsprintf(buf, fmt, arg);
    write(buf, i);
    return i;
}

vsprintf函数:

int vsprintf(char *buf, const char *fmt, va_list args)
{
    char *p;
    char tmp[256];
    va_list p_next_arg = args;
    for (p = buf; *fmt; fmt++)
    {
        if (*fmt != '%')
        {
            *p++ = *fmt;
            continue;
        }
        fmt++;
        switch (*fmt)
        {
        case 'x':
            itoa(tmp, *((int *)p_next_arg));
            strcpy(p, tmp);
            p_next_arg += 4;
            p += strlen(tmp);
            break;
        case 's':
            break;
        default:
            break;
        }
        return (p - buf);
    }
}

printf函数接受不定数量的参数va_list arg, 然后调用vsprintf.

vsprintf函数遍历fmt即格式字符串, 生成字符串存入buf,
然后返回字符串的长度.

返回printf函数后接着调用系统函数write打印i长度的字符串.

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

write向寄存器传入数值, 然后设置中断门, 通过系统调用syscall

sys_call:
     call save
     push dword [p_proc_ready]
     sti
     push ecx
     push ebx
     call [sys_call_table + eax * 4]
     add esp, 4 * 3
     mov [esi + EAXREG - P_STACKBASE], eax
     cli
     ret

syscall将字符直到’\0’的ascii码值通过总线传到显示芯片的存储空间中.
字符显示驱动子程序通过ascii码在字体苦衷查找点阵信息,
将点阵信息存储在vram中.
显示芯片按照刷新频率逐行读取vram,并通过信号线向显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

当程序调用getchar时,等待键盘输入硬件中断。

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章介绍了linux IO设备管理方法和Unix
IO接口,理解Linux一切皆文件的思想并深入分析了printf的实现与getchar的实现。

结论

Hello的一生平凡而波澜壮阔,看似简单的一个小程序,背后是计算机系统发展的深厚技术积淀。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的运行需要虚拟内存的概念,通过一系列复杂的过程hello能够由虚拟内存地址转换为物理地址并访问之。

  7. hello的输入输出与外界交互,与linux I/O息息相关

  8. hello最终被shell父进程回收,内核会收回为其创建的所有信息

总而言之,只是输出一句简单的hello
world,背后却有那样多复杂的技术细节,而我们今天探讨的,还只是计算机底层机制的冰山一角。这样多的底层机制互相合作,又得益于诸如虚拟内存,进程,unixIO,高级程序设计语言等等成功的抽象,使得计算机技术"飞入寻常百姓家",让hello
world成为无数程序员的第一条代码。惊叹于计算机系统的复杂与神奇的同时,我们也要更认真的学习相关知识,真正理解计算机系统的原理和精髓。

附件

  1. 源代码hello.c

  2. 经过预处理器生成的修改了的源文件hello.i

  3. 经过编译器生成的汇编语言文件hello.s

  4. 经过汇编器生成的可重定位目标文件hello.o

  5. 经过链接器生成的可执行目标文件hello

  6. 链接器生成的过程verbose.txt

  7. hello.o通过objdump得到的反汇编文件asmhello.txt

  8. hello通过objdump得到的反汇编文件asmhello2.txt

参考文献

[1] Bryant, Randal E., and David Richard O’Hallaron. Computer
systems: a programmer’s perspective
. Prentice Hall, 2011.

[2] https://www.cnblogs.com/pianist/p/3315801.html

[3] https://fengmuzi2003.gitbook.io/csapp3e

[4] https://hansimov.gitbook.io/csapp

[5] 编译原理, 作者: Alfred V. Aho / Monica S.Lam / Ravi Sethi /
Jeffrey D. Ullman

出版社: 机械工业出版社

[6]
https://www.cnblogs.com/gmpy/p/10702150.html#:~:text=%E9%80%BB%E8%BE

%91%E5%9C%B0%E5%9D%80%EF%BC%88logical%20address%EF%BC%89%EF%BC%9A,%E6%8A%8A%E7%A8%8B%E5%BA%8F%E5%88%86%E6%88%90%E8%8B%A5%E5%B9%B2%E6%AE%B5%E3%80%82

  • 21
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值