HITICS-2019-FinalHW

HITICS-2019-FinalHW

摘 要
此文章牢牢以hello从诞生到被回收的过程为主线, 围绕hello这一个程序, 按照顺序实践了预处理、编译、汇编、链接、进程管理、储存管理、IO管理等操作,并对各个操作的细节、涉及到的核心计算机系统知识以及其他细节进行了详尽的分析。本文通过利用hello为例总结这些核心计算机系统知识板块,能为计算机系统初学者对知识的理解和整体串联带来益处。

关键词:预处理;编译;汇编;链接;进程管理;储存管理;I/O管理;虚拟
机;EDB Debugger工具;反汇编语言;elf格式分析

目 录

第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 5 -
第2章 预处理 - 5 -
2.1 预处理的概念与作用 - 5 -
2.2在UBUNTU下预处理的命令 - 5 -
2.3 HELLO的预处理结果解析 - 6-7 -
2.4 本章小结 - 8 -
第3章 编译 - 9 -
3.1 编译的概念与作用 - 9 -
3.2 在UBUNTU下编译的命令 - 9 -
3.3 HELLO的编译结果解析 - 9-15 -
3.4 本章小结 - 16 -
第4章 汇编 - -
4.1 汇编的概念与作用 - 17 -
4.2 在UBUNTU下汇编的命令 - 17 -
4.3 可重定位目标ELF格式 - 17-21 -
4.4 HELLO.O的结果解析 - 21-23 -
4.5 本章小结 - 24-25 -
第5章 链接 - 26 -
5.1 链接的概念与作用 - 26 -
5.2 在UBUNTU下链接的命令 - 26 -
5.3 可执行目标文件HELLO的格式 - 26-32 -
5.4 HELLO的虚拟地址空间 - 33 -
5.5 链接的重定位过程分析 - 33-36 -
5.6 HELLO的执行流程 - 36 -
5.7 HELLO的动态链接分析 - 36-37 -
5.8 本章小结 - 37 -
第6章 HELLO进程管理 - 38 -
6.1 进程的概念与作用 - 38 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 38 -
6.3 HELLO的FORK进程创建过程 - 38-39 -
6.4 HELLO的EXECVE过程 - 39 -
6.5 HELLO的进程执行 - 39-40 -
6.6 HELLO的异常与信号处理 - 40-44 -
6.7本章小结 - 45 -
第7章 HELLO的存储管理 - 46 -
7.1 HELLO的存储器地址空间 - 46 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 46-47 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 47 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 47-48 -
7.5 三级CACHE支持下的物理内存访问 - 48 -
7.6 HELLO进程FORK时的内存映射 - 48 -
7.7 HELLO进程EXECVE时的内存映射 - 48 -
7.8 缺页故障与缺页中断处理 - 49 -
7.9动态存储分配管理 - 49-51 -
7.10本章小结 - 51 -
第8章 HELLO的IO管理 - 52 -
8.1 LINUX的IO设备管理方法 - 52 -
8.2 简述UNIX IO接口及其函数 - 52 -
8.3 PRINTF的实现分析 - 52-54 -
8.4 GETCHAR的实现分析 - 54-55 -
8.5本章小结 - 55 -
结论 - 56-57 -
附件 - 57-58 -
参考文献 - 59 -

第1章 概述
1.1 Hello简介
1.1.1 hello的Program to Process
GCC C编译器ccl驱动程序读取源程序文本文件hello.c
预处理器(cpp)预处理, 得修改后的源程序文本文件hello.i
编译器(ccl) 编译,得汇编程序文本文件hello.s
汇编器(as)汇编,得可重定位目标程序(二进制)hello.o
链接器(ld)把hello.o和printf.o链接生成可执行目标程序(二进制)hello
壳(Bash)中,进程管理(OS)调用fork函数创建新子进程;
使用mmap把它映射至虚拟内存;
虚拟地址被TLB、Cache(3级)、页表(4级)转换为物理地址;
execve函数被调用加载可执行文件hello;
hello被分时操作系统分配时间片;
hello指令被取指译码执行
1.1.2 hello的Zero-0 to Zero-0过程
hello.c由用户在空白文本编辑界面编写代码而来,用户编写之前,是0的状态。
用户经过编辑器编写,从0得到hello.c,hello.c经过(1)中过程,进入hello可执行目标程序的被执行阶段,在操作系统中以进程的形式运行,终止后被父进程回收,被内核从系统清除。
终止后在系统中被内核清除后,hello又回到了0的状态。
1.2 环境与工具
Win10 64bits VmwareWorkstation Ubuntu 18.04
X64 CPU, 2GHz, 2G RAM, 256GHD Disk
GDB GCC CodeBlocks 17.12
1.3 中间结果
hello.i:
由cpp预处理hello.c文件所得,可由ccl编译
hello.s:
cclo编译hello.i所得,可由as汇编
hello.o:
as汇编hell.s所得,是可重定位目标程序(二进制)
hello:
ld链接hello.o和printf.o所得,是可执行目标程序(二进制)
hello.elf:
hello.o的elf格式
ldhello.elf:
hello的elf格式
objdumphello:
反汇编hello.o所得
objdumpldhello:
反汇编hello所得
1.4 本章小结
hello的P2P过程就是hello.c从源程序文本文件经过一系列的处理生成可执行文本文件,再进入系统由进程的形式执行的过程。
hello的020过程就是一个从无到有再到无的过程:从还没被用户编写代码的空白状态,到生成可执行文件并被执行,最终到进程终止被回收、清楚痕迹又成了无的状态。
本论文中所有实践所依托的软硬件环境、开发调试工具以及生成的中间文件得到了总结。

第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理的概念
预处理是预处理器编译源程序文本文件生成汇编程序之前对源程序文本文件进行的修改。
2.1.2 预处理作用
预处理对源程序文本文件(如hello.c)中的源代码进行扩展,插入所有用#include命令指定的文件、扩展所有用#define声明指定的宏,得到修改后的源程序文本文件(如hello.i),以.i作扩展名。
2.2在Ubuntu下预处理的命令
在Ubuntu 18.04中打开hello.c所在目录的终端,输入以下命令行,得到hello.i文件

图2-1:Ubuntu下预处理hello.c过程
命令行中,gcc代表启用GCC C编译器。
2.3 Hello的预处理结果解析
预处理的作用是: 预处理对源程序文本文件(如hello.c)中的源代码进行扩展,插入所有用#include命令指定的文件、扩展所有用#define声明指定的宏,得到修改后的源程序文本文件(如hello.i),以.i作扩展名。
首先是源代码的扩展,打开hello.i观察代码:

图2-2 hello.i中对应hello.c#include、#define命令的扩展部分(部分)

即cpp预处理hello.c,插入了所有用#include命令指定的文件、扩展了所有用#define声明指定的宏。
其次是生成修改后的源程序文本文件并以.i作常用后缀名,由下图3可见,hello.c被预处理后在同目录下生成了hello.i文件。

图2-3 预处理生成hello.i文件
2.4 本章小结
预处理是预处理器编译之前,对源程序文本文件进行的修改,这种修改包括插入、扩展等。
预处理对源程序文本文件(如hello.c)中的源代码进行扩展,插入所有用#include命令指定的文件、扩展所有用#define声明指定的宏,得到修改后的源程序文本文件(如hello.i),以.i作扩展名。
hello.c在Ubuntu下的预处理操作过程是在终端输入指令,得到hello.i文件。

第3章 编译
3.1 编译的概念与作用
3.1.1 编译的概念
编译在这里指编译器把经预处理后的源程序文本文件处理成汇编程序文件的过程。
3.1.2 编译的作用
编译器(这里是ccl)编译预处理后的文件(这里是hello.i),产生源文件的汇编代码,生成相应的汇编文件(.s),以方便汇编器的进一步处理。
3.2 在Ubuntu下编译的命令
在Ubuntu中hello.i所在目录下,打开终端,输入Unix命令行编译,即把hello.i生成了hello.s文件保存在同目录中。

图3-1 编译hello.i的命令

Ubuntu下编译命令一般包含gcc(gcc c compiler)、.i文件名称、目标.s文件名称等等。
3.3 Hello的编译结果解析
此部分说明编译器是如何处理C语言各个数据类型和操作的。
3.3.1 常量
hello.c中,有各种常量,如循环中的循环上限常量(i < 10中的10),行18、23 printf的输出内容中的字符串常量。
对于C语言直接用数字指明的常量,比如发if(argc!=3)中的3,编译器的处理是用十六进制的常量替代之,符号表示为$0x3或者$3 ,如图3-2

图3-2 编译器对C直接用数字指明的常量的处理

对其他具体数字类型的常量如浮点数常量,编译器也有类似的处理方式。
另外,对于除了一般意义上数字常量之外,还有字符串常量,在hello.c中,如图3-2printf的输出,就是C表达的字符串常量

图3-3 hello.c中的字符串常量
hello.s中,对应内容

图3-4 hello.s中的字符串常量

也就是说,编译器对于字符串常量中的符号并没有作特殊的转码,仍保留了其原来的样式(但是汉字除外,如图3-2printf最后一个感叹号是汉字格式,那么编译器会把它转化成对应的三位一组的特殊数字码 \357\274\201)。但在格式上在行前用.string清楚地指明是字符串常量。
3.3.2 变量
hello.c中的变量有sleepsecs,argc,i。
对于sleepsecs,编译器的处理如下:

图3-5 sleepsecs在hello.s中的形式
可以看出,编译器对于像sleepsecs这种全局变量的处理方式是,放到.data段,且用对应大小的字节对齐(这里是4字节,因为sleepsecs被声明为int整型)
对于argc:

图3-6 main函数参数argc(整型变量)在hello.s中的形式
可见,编译器处理函数参数的方式,是通过mov类指令把它放到相应的寄存器中。这里main的第一个参数argc被放到%edi
对于i:

图3-7 局部变量i在hello.s中的形式
编译器对局部变量的处理方式是通过mov类指令把它保存在相应寄存器中(这里是对X86-64处理器的整数寄存器而言)。
3.3.3类型转换
C中存在显式、隐式类型转换。编译器在处理hello.c中int sleepsecs = 2.5(发生了隐式类型转换,浮点数2.5隐式地转换成了2 int整型)时,在hello.s中数值上直接显示转换后的数值2而不是2.5。编译器不会把类型转换的细节体现出来,而是直接在汇编文件中显示转换完成后的数值结果。

图3-8 sleepsecs的赋值汇编代码
3.3.4 数组
hello.c中唯一的数组是char* argv。它以main函数参数的形式输入main函数中。

图3-8 hello.c中的数组

图3-9 argv数组在hello.s汇编代码中的对应
可见数组被存在%rsi,这是因为它是作为main的第二参数输入main 中的。可见,编译器在处理数组的时候,
3.3.5 赋值
hello.c中有两个赋值, i = 0以及sleepsecs = 2.5.
i = 0对应hello.s汇编代码为:

		图3-10  hello.c 中i=0赋值操作在hello.s中的对应

sleepsecs = 2.5对应hello.s汇编代码为:

图3-11 hello.c中sleepsecs = 2.5的赋值操作在hello.s中的对应

可见, 编译器对局部变量的赋值处理方式, 类似于对局部变量存储的方式, 使用mov类指令把值存储到局部变量所在的存储器中. 这个值可以是常数, 用$+数字表示, 也可以是变量、表达式,用其值所在的寄存器符号表示。
编译器对全局变量赋值的处理方式, 是在全局声明部分(main函数之前),用
全局变量:
.数据类型 数值
的格式进行的。
总结起来可以发现,编译器对C赋值表达式的处理方式,和编译器储存C变量方式相同,因为在赋值的同时也顺带储存了,所以在形式上是相似的。
3.3.6 操作符
3.3.6.1 算术的操作
hello.c中,i++是唯一的算数操作

图3-12 i++在hello.c中

对应hello.s中的汇编代码为:

图3-13:hello.c i++ 算数表达式在hello.s中汇编代码的对应

可见编译器对C的算数操作一般是通过与和算数操作中设计的数值所在的寄存器相关的汇编命令表示。
观察到hello.s中还有其他算数运算命令,比如

图3-14 hello.s中一些算数运算汇编代码命令

所以C的算数操作代码其实少于编译器编译出的汇编代码中的算数操作,这是因为编译器把程序语言向更底层转换成汇编代码,进而体现出更底层的具体实现的结果。

3.3.6.2 关系的操作
hello.c中关系的操作有i<10和argc!=3. 对应hello.s中汇编代码为:

图3-15 hello.c中关系操作在hello.s中的对应

由此知编译器对C关系操作的处理主要是通过cmp类的汇编指令表达的,是将存储在寄存器中的关系操作对象同常数或其他寄存器中对象比较的。一般cmp指令后会有跳转指令尾随,这是对C中条件控制语句的编译结果。
3.3.7 函数
hello.c中一共用了这些函数:
main,printf,exit,getchar,sleep
它们在hello.s中的对应汇编代码如下

图3-16 hello.c中调用函数以及main的返回对应于hello.s的汇编代码
编译器在处理C对函数的操作时,用call 函数名@PLT 这样格式的汇编代码来表示。有一点不同的时候,对于printf,hello.c中的第一个printf对应汇编代码时puts,这是因为此printf没有像%s这种引用符,而是直接输出常量字符串。
对于函数的参数传递来说,第n个参数有对应的寄存器来存储。e.g., main函数第一个参数argc存到%edi里,第二个参数argv存到%rsi里。对于字符串的输出而言,需要设置字符串的首地址便于输出字符串。例如printf(第一个),就把%edi设置为了字符串的首地址。对于第二个printf,数组元素argv[1] argv[2]分别存到了register %rsi和%rdx里面。对于sleep(),参数通过%edi传递(sleepsecs)。
和所有函数一样,编译器在处理函数返回的时候会将返回值存到%rax这个寄存器里。hello.c main返回0的时候就是通过movl $0, %eax指令这样做的。ret代表了C的return。

3.3.8 控制转移

图3-17 hello.c C代码中的控制转移

hello.c中的C代码有两个控制转移:一个是if的条件控制(argc != 3)、一个是for的条件控制(i < 10)。
对应的编译后的汇编代码为:

图3-18 hello.s 汇编代码中的控制转移

控制转移条件是把寄存器里面的存的地址处的数值和常数值(当然也可以和寄存器里面存的数值、寄存器里面存的地址的数值)比较,然后尾随一个跳转条件指令,以完成对C中条件控制转移的同义翻译。
3.4 本章小结
编译在这里指编译器把经预处理后的源程序文本文件处理成汇编程序文件的过程。
编译器(这里是ccl)编译预处理后的文件(这里是hello.i),产生源文件的汇编代码,生成相应的汇编文件(.s),以方便汇编器的进一步处理。
Ubuntu下编译命令一般包含gcc(gcc c compiler)、.i文件名称、目标.s文件名称等等。
编译器编译C各种数据类型和操作的时候,具有一定共性但又各不相同。

第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
汇编是指汇编器(as)将.s文件的汇编语言指令翻译为机器语言指令并生成.o机器语言二进制程序的过程。
4.1.2 汇编的作用
汇编可以将汇编语言文件(.s)指令翻译为机器语言指令,整合为可重定位目标程序文件(.o),以供链接器(ld)将其和其他文件链接生成可执行目标程序(二进制)。
4.2 在Ubuntu下汇编的命令

图4-1 Ubuntu下汇编hello.s生成hello.o的指令
4.3 可重定位目标elf格式
分析hello.o的ELF格式前, 先用终端将其转换成ELF格式. 输入如下命令即可得到hello.elf文件:
readelf -a hello.o > hello.elf
打开elf文件, 得到下列信息:

1) ELF Header

图4-2 ELF头

ELF头显示以下信息:
数据: 补码 小端
操作系统/ABI: UNIX-SYSTEM V
机器: x86-64
ELF头的大小: 64字节
节头大小: 64字节
节头数量: 13
程序头数量: 0
ELF类型: REL
目标文件类型(可重定位文件)
等.

2)节头(Section Headers)

图4-3 hello.elf的节头

节头显示各节的以下信息:
名称
大小
类型
链接
信息
对齐
地址
旗标
全体大小
偏移量

3)重定位节(Relocation Section(

图4-4 hello.elf .rela.text的重定位节

.rela.text的重定位节,有以下信息:
信息
类型
加数
符号名称
符号值
偏移量

图4-5 hello.elf .rela.eh_frame的重定位节

.rela.eh_frame的重定位节,亦有以下信息:
偏移量
信息
类型
符号名称
符号值
加数

重定位过程解析:
选择一个对象, 比如函数printf, 找到对应行;
读取偏移量为00000000005a, R_X86_64_PLT3, 加数为-4;
.text代码段地址4为引用地址. 最终地址为+0x00000000005a.

  1. 符号表Symbol Table

图4-6 hello.elf的符号表

符号表由以下信息:
Num 序号
Value 值
Size 大小
Type 类型
Bind 全局或局部变量
Name 名称
Vis
Ndx
信息读取:
以对象类型sleepsecs为例, 由表中信息可知: 大小为4字节,偏移量为0, 其Ndx = 1, 位于.text中. 其他对信息的读取可参照此.
Ndx的内容需要和节头关联获取.

5)程序头Programme Header

图4-7 本文件无程序头

高亮语句指明:本文件中没有程序头
4.4 Hello.o的结果解析
4.4.1 反汇编代码查看
终端输入objdump -d -r hello.o, 查看hello.o的反汇编code:

图4-8 hello.o的反汇编文件

4.4.2 hello.o结果解析、和hello.s的代码对照分析
由第三章, hello.s文件如下图:

图4-9 hello.s文件
4.4.2.1 机器语言的构成
因为本章讨论汇编, 故说明汇编语言的构成,可归纳如下:

  1. 汇编指令(机器码的助记符)
    2)伪指令(没有对应的机器码,由编译器执行,计算机并不执行)
    3)其他符号(如+、-、*、/,由编译器识别)
    4.4.2.2 与汇编语言的映射关系
    1)操作数不一致
    hello.o生成的机器语言中,操作数由十六进制显示,

图 hello.o的objdump文件操作数十六进制显示实例
hello.s中则是十进制。

图4-10 hello.s文件操作数十进制显示实例

2) 分支转移
	二者分支转移的方式主要体现在跳转地址的表达上。hello.o生成的机器语言中,直接利用目的地址进行跳转:

图4-11 hello.o 反汇编的分支转移表示

图4-12 hello.s的分支转移表示
hello.s则是用助记符.Ln来表示:
3)函数调用
二者函数调用的方式差别体现在调用函数的指代上, 即hello.o反汇编用地址来指代, hello.s中用函数名来指代:

图4-13 二者函数调用的对比
4.5 本章小结
汇编是指汇编器(as)将.s文件的汇编语言指令翻译为机器语言指令并生成.o机器语言二进制程序的过程。
汇编可以将汇编语言文件(.s)指令翻译为机器语言指令,整合为可重定位目标程序文件(.o),以供链接器(ld)将其和其他文件链接生成可执行目标程序(二进制)。
用指令gcc -c hello.s -o hello.o在Ubuntu下即可生成机器语言二进制程序。
elf格式包括了ELF 头、节头、程序头、重定位节和符号表。
hello的汇编结果在hello.o文件可以体现,用objdump指令查看了它的反汇编代码,从分支转移、函数调用和操作数等将其和hello.s对比分析了差异。

第5章 链接
5.1 链接的概念与作用
5.1.1链接的概念
链接器(ld)将可重定位目标程序链接为可执行目标程序
5.1.2链接的作用
链接器(ld)处理文件的合并得到可执行目标文件,以便其被加载到内存中被系统执行。
5.2 在Ubuntu下链接的命令
Ubuntu下,输入如图指令调用ld对hello.o进行链接处理,得到可执行目标文件hello

图5-1 Ubuntu下对hello.o的链接命令
5.3 可执行目标文件hello的格式
本节将分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
首先需要得到链接后的elf hello文件,在控制台输入以下命令:

图5-2 Ubuntu下得到链接后文件的elf格式文件

打开生成的hello-ld.elf文件,各部分解读如下:

  1. ELF头

图5-3 hell-ld.elf文件

ELF头的信息:
目标文件的类型: 可执行文件
机器:x86-64
ELF头大小:64字节
程序头大小:56字节
节头大小:64字节
节头数量:25

2) 节头

图5-4 节头信息

节头有信息:
序号 Nr
名称 Name
类型 Type
地址 Address
偏移量 Offse
3) 程序头

图5-5 程序头信息

程序头有信息:
类型 Type
偏移量 Offset
虚拟地址 VirtAddr
物理地址 PhysAddr
4) 段节

图5-6 段节信息
段节有信息:
序号
对应信息
5)动态节

图5-7 动态节信息

动态节有信息:
标签 Tag
类型 Type
名称/值 Name/Value
6)重定位节

图5-8 重定位节信息

重定位节包括
Offset 偏移量
Info 具体信息
Type 类型
系统值 Sym. Value
系统名称+Addend
等信息

7)符号表

图5-9 符号表信息
符号表包含
序号 Num
值 Value
大小 Size
类型 Type
全局或局部信息 Bind
Vis
Ndx
名称 Name
等信息
8)桶列表长度直方图

图5-10 桶列表长度直方图
Histogram for bucket list length包含
长度 Length
数量 Number
总覆盖率 Total Coverage(%)
等信息
9)版本符号部分

图5-11 版本符号部分

版本符号部分包括
地址 Addr
偏移量 Offset
等信息
10)版本需求部分

图5-11 版本需求部分

版本需求部分包括
地址 Addr
偏移量 Offset
等信息

5.4 hello的虚拟地址空间
5.4.1 EDB查看hello
用EDB查看hello,查看本进程的虚拟地址空间各段信息:

图5-12 hello的虚拟地址空间各段信息
5.4.2 对照解读
与5.3对照分析,仔细观察5.3中Program Header每个部分的地址,注意到EDB打开的hello加载的信息首地址是0X4000000,发现各部分所占地址区间和程序头是一致的

图5-13 EDB打开的hello文件.ELF头部分信息
5.5 链接的重定位过程分析
控制台输入objdump -d -r hello

图5-14 hello 反汇编文本

仔细观察分析代码可以发现hello和hello.o反汇编结果的不同。
1)hello.o的反汇编有hello所没有的几个节:
Disassembly of section .init
Disassembly of section .plt
Disassembly of section .text
Disassembly of section .fini
并且库函数代码链接到了程序中。
2)二者调用函数时对函数地址指代的方式不同
hello是直接指代函数地址;
hello.o是利用地址偏移量来call函数。

3)二者跳转时对跳转地址指代的方式不同
hello直接用地址来跳转,hello.o用偏移量来跳转,这一点和函数调用类似。
5.6 hello的执行流程
使用edb执行hello,展示了从加载hello到_start,到call main,以及程序终止的所有过程,列出了其调用与跳转的各个子程序名:
_dl_start
_dl_init (加载)
_stat
_cax_atexit
_new_exitfn
_libc_start_main
_libc_csu_init (Execution)
_main
_printf
_exit
_sleep
_getchar
_dl_runtime_resolve_xsave
_dl_fixup
_dl_lookup_symbol_x (Running)
_exit (程序终止)
5.7 Hello的动态链接分析
本节分析了hello程序的动态链接项目。通过edb调试,分析了在dl_init前后,这些项目的内容变化。
用EDB Debugger调试 在dl_init前后设置breakpoint如图:

图5-15 dl_init断点设置
在callq ld-2.27. so! dl_init 这一行前后设置了断点
紧接着需要确定一个hello程序的动态链接项目,观察hello的反汇编文件内容,确定为全局偏移表,找到其位置为000000000601000

图5-16 全局偏移表地址寻找

用EDB的Data Dump查看对应地址的数据,分为执行dl_init前和之后,分别如图所示:

图 5-17 dl_init执行后

图5-18 dl_init执行前

对比可以看出,执行dl_init前后,全局偏移表的值发生了变化。因此可以得出结论: dl_init通过初始化赋值(函数地址)把相关被调用的函数链接到了动态库中。
5.8 本章小结
链接器(ld)将可重定位目标程序链接为可执行目标程序。
链接器(ld)处理文件的合并得到可执行目标文件,以便其被加载到内存中被系统执行。
对于可执行目标文件hello的格式,从ELF头、节头、程序头、段节、动态节、重定位节、符号表、桶列表长度直方图、版本符号部分、版本需求部分进行了分析。
使用objdump查看机器语言,针对hello与hello.o的不同进行了分析,这种不同有函数调用地址表达、分支条件结构的表达等。使用edb执行hello,展示了从加载hello到_start,到call main,以及程序终止的所有过程。使用EDB Debugger调试,分析了hello的动态链接。

第6章 hello进程管理
6.1 进程的概念与作用
6.1.1进程的概念
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。。
6.1.2进程的作用
现代系统通过进程来使得程序看似系统中唯一运行的、唯一占领CPU和Memory的,并且是CPU按顺序处理指令的对象、其数据和指令是内存中唯一对象。这是一种由进程提供的假象。
(Ref: CSAPP 3e p.508)
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 作用
输出一个命令行提示符,执行用户输入的命令行。
6.2.2 处理流程
1)如果能够成功找到命令,那么该命令将被分解为系统调用传给Linux内核。
2)如果该命令行的第一个单词不是一个内置的shell命令,shell就会假设这是一个可执行文件的名字,并将加载并运行这个文件。
3)如果命令既不是一个内置的shell命令也没有找到该可执行文件,则会提示错误。

(Ref: CSAPP 3e p.5)
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的运行的子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。fork函数调用一次,返回两次,一次是在父进程中,返回子进程的PID,一次是在子进程中,返回0。因为子进程的PID总非0,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。父进程和子进程是并发运行的,也就是说在不同的机器上,运行的先后顺序不一样。
(Ref: CSAPP 3e p.513, 514, 515)
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序,函数结构如下int
execve(const char *filename, const char *argv[],const char *envp[]);execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。所以,与fork一次调用返回两次不同,execve调用一次从不返回。
加载成功后,当加载器运行时候,它创建类似于图83的内存映像。在程序头部表的引导下,加载器将可执行文件的片(chunk)复制到代码段和数据段。接下来,加载器跳转到程序的入口点,也就是_start函数的地址。这个函数是在系统目标文件ctrl.o中定义的,对所有的C程序都是一样的。_start函数调用系统启动函数__libc_start_main,该函数定义在libc.so中。它初始化执行环境,调用用户层的main函数,处理main函数的返回值,并且在需要的时候把控制返回给内核。
(Ref: CSAPP 3e p.484, 521)
6.5 Hello的进程执行
6.5.1 进程执行的分类简介
1)时间片
即一个进程执行它的控制流的一部分的每一时间段。
2)逻辑控制流
一系列程序计数器PC的值的序列叫做逻辑控制流,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。
3)上下文切换
上下文就是内核重新启动一个被抢占的进程所需要的状态,是一种比较高层次的异常控制流。开始Hello运行在用户模式,收到信号后进入内核模式,运行信号处理程序,之后再返回用户模式。运行过程中,CPU不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用CPU,实现进程的调度。
4)用户模式和内核模式
shell使得用户可以有机会修改内核,所以需要设置一些防护措施来保护内核,如限制指令的类型和可以作用的范围。
6.5.2 进程执行的详细分析
内核为每一个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需要的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表,包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程主要分为以下三步:保存当前进程的上下文、恢复之前某个被抢占的进程的被保存的上下文、将控制传递给这个新恢复的进程。
例如sleep系统调用时,它显式地请求让调用进程休眠。此外所有系统都有产生周期性定时器中断的机制(通常为每1毫秒或每10毫秒)。每次发生定时器中断时,内核就能判定当前进程已经运行了足够长的时间,并切换到另一个新进程。当发生上下文切换时,处理器将模式从用户模式转换到内核模式,切换完毕后,会从内核模式转换回用户模式,如下图84所示,调用sleep函数时会发生的上下文切换以及用户模式和内核模式的转换。
(Ref: CSAPP 3e p.511)
6.6 hello的异常与信号处理
6.6.1 hello执行过程中出现的异常
类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令
终止 同步 不返回
图6-1 hello执行过程中的异常
6.6.2 hello执行过程中出现的信号
执行过程中按不同键,按次序出现以下情况:
1) 执行过程中按回车
执行过程中按回车

图6-2 hello执行过程中信号分析(1)
解读:
hello执行时会不停输出字符串“11803010412 Davvv”。输出过程中若按回车,则会使输出换行。并且输入的回车符会停留于缓冲区中,进程终止后释放。

2)执行过程中按ctrl+c

图6-3 hello执行过程中信号分析(2)

解读:
按ctrl+c会终止进程的前台工作,回到等待命令行输入的最初阶段。

3)执行过程中按ctrl+z

图6-4 hello执行过程中信号分析(3)
解读:
执行过程中按ctrl+z,进程前台作业将被停止(注意,停止不等于终止,停止在这里可以理解为暂停)。输出的提示命令行会带有表达停止次数的序号。
,然后jobs查看当前暂停的进程可以看到hello,利用kill杀死hello进程后,ps显示进程看不到hello,fg也不能恢复hello进程

5) 按ctrl+z之后再输入ps命令行

图6-5 hello执行过程中信号分析(4)

解读:
输入ps命令行,显示了进程的进程ID、TTY、时间、命令等信息。

6) 按ctrl+z后再按jobs
图6-6 hello执行过程中信号分析(5)

解读:
按ctrl+z后再按jobs,将会显示被停止的所有进程序号。这里因为按ctrl+z两次,停止了两次,故显示如上图。

7)kill
图6-7 hello执行过程中信号分析(6)

解读:
kill PID为3504的进程。再用ps显示,则未出现PID是3504的进程了。再kill PID 为3505的进程,再用ps显示,也未出现PID为3505的进程了。输入jobs查看,无反应,和(6)对比,这说明进程确实被kill指令杀死了。

7)fg

图6-8 hello执行过程中信号分析(7)
在这里要分三种情况,如果是在kill掉进程之后使用fg指令,则因为job已经被kill掉了,故会提示no such job;若是在键入ctrl+z之后使用fg指令,工作将会被恢复,如图:

图6-9 hello执行过程中信号分析(8)

而若是再输入ctrl+c之后输入fg企图恢复,则无效:

图6-10 hello执行过程中信号分析(9)

7) pstree

图6-11 hello执行过程中信号分析(10)

解读:
在输入ctrl+z之后输入pstree指令可以查看当前进程进程树
6.7本章小结
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。。
现代系统通过进程来使得程序看似系统中唯一运行的、唯一占领CPU和Memory的,并且是CPU按顺序处理指令的对象、其数据和指令是内存中唯一对象。这是一种由进程提供的假象。
另外,本章叙述了壳Shell-bash的作用与处理流程;叙述了Hello的fork进程创建过程以及execve过程。本章最后通过具体的异常、信号、信号处理和实操解读了hello的进程执行过程。

第7章 hello的存储管理
7.1 hello的存储器地址空间
虚拟地址:
1) 程序访问存储器所使用的逻辑地址称为虚拟地址。
2) 在hello中,对应地是hallo的虚拟内存地址。
物理地址
1) 计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组,每一个字节单元给以一个唯一的存储器地址,称为物理地址。
2) 在hello中,对应地是hello中的虚拟内存地址对应的物理地址。
线性地址:
线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
逻辑地址:
1) 在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址(物理地址)。
2) 在hello中,对应地是hello.o中的相对偏移地址。
(Ref:CSAPP 3e p.560)
7.2 Intel逻辑地址到线性地址的变换-段式管理
7.2.1 Intel逻辑地址到线性地址的变换流程总结
获得逻辑地址 ( 段选择符:段内偏移地址)
 check段选择描述符T1字段,
1)若是0
转换对象为GDT中的段
2)若是1
转换对象为LDT中的段
由相应寄存器,获取地址、大小,存储在一个数组内。
利用段选择符前13位信息,找到存储在数组中的段描述符,获取基地址
利用线性地址=基地址+偏移量 公式计算出线性地址
7.2.2段式管理
1)段式管理通过寄存器来进行
段寄存器用于存放段选择符,通过段选择符可以得到对应段的首地址。处理器在通过段式管理寻址时,通过段描述符得到段基址,再与偏移量结合得线性地址,最终得虚拟地址。
2) 段式管理的分类
根据段式管理在不同模式下的区别, 可以分为保护模式和实模式下不同的段式管理。
保护模式:
线性地址需逻辑地址通过段机制得到,线性地址经过分页机制才可得物理地址。
实模式:
逻辑地址、线性地址和实际的物理地址相同。段寄存器存放真实段基址、给出32位地址偏移量,则可访问真实物理内存。
7.3 Hello的线性地址到物理地址的变换-页式管理
7.3.1 Hello的线性地址到物理地址的变换流程总结
获取进程的页目录地址(from CR3),通过其20个bit获取页目录基地址。
根据上述基地址以及线性地址的前10个bit,进行组合得到线性地址的前十位的索引对应的项在页目录中地址,根据该地址可以取到该地址上的值,该值就是二级页表项的基地址
取上一步所得地址20个bit,把线性地址的10~19位左移2位,组合获取物理页框(该线性地址对应的)在内存中地址位于二级页表中的起始地址
上一步所得地址沿继读4字节,获取物理页框(该线性地址对应的)在内存中地址位于二级页表中的地址,which is物理页框(线性地址对应的)在内存中的基地址
截取上一步所获取的基地址的前20个bit,获取物理页框在内存中基址
取线性地址末12位偏移量获取物理地址,存储在该地址的值即为结果。
7.3.2 页式管理
系统将每个段分割为被称为虚拟页(SIZE: 4KB)的大小固定的块来作为进行数据传输的单元.
物理内存也被分割为物理页,虚拟内存系统中MMU负责地址翻译,MMU使用存放在物理内存中的被称为页表的数据结构将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。
7.4 TLB与四级页表支持下的VA到PA的变换
7.4.1 TLB支持下

CPU产生VirtAddr
PTE被MMU从TLB取出来
MMU完成VirtAddr到PhysiAddr的转换
该物理地址被发送至SRAM的Cache或者DRAM的主存
CPU收到SRAM的Cache或者DRAM的主存Request得到的数据字
7.4.2 四级页表支持下

  1. VirtAddr  4 VPN + 1 VPO
    1. 每个VPNi都是一个到第i(1≤i≤4)级页表的索引
    2. 第j(1≤j≤3)级页表中的每个PTE都指向j+1级的某个页表的基址。
    3. 第4级页表中的每一个PTE包含某个物理页面的PPN,或者一个磁盘块的地址
    4. PPO和VPO相同。
      7.5 三级Cache支持下的物理内存访问
      1)拆分: 物理地址 = CT(标记) + CI(Cache组索引) + CO(块偏移)
      2) 一级Cache:若命中,返回;若不命中,没有标记位为有效字节的与之匹配,则访问二级Cache
      3)二级Cache: 若命中,返回;若不命中,没有标记位为有效字节的与之匹配,则访问三级Cache
      4)三级Cache:若命中,返回;若不命中,没有标记位为有效字节的与之匹配,则访问DRAM主存。
      5)主存:若缺页,则访问磁盘。
      7.6 hello进程fork时的内存映射
      当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
      当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。因此,也就为每个进程保持了私有地址空间的抽象概念。
      (Ref:CSAPP 3e p.584)
      7.7 hello进程execve时的内存映射
       运行execve(“hello”, NULL, NULL)
       加载、运行hello.out,
       删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
       映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
       映射共享区域。如果hello程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
       设置PC。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
      下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面
      (Ref:CSAPP 3e p.585)
      7.8 缺页故障与缺页中断处理
      在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页。例如:CPU引用了VP3中的一个字,VP3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE3,从有效位推断出VP3未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4。如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4的页表条目,反映出VP4不再缓存在主存中这一事实
      接下来,内核从磁盘复制VP3到内存中的PP3,更新PTE3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在VP3已经缓存在主存中了,那么也命中也能由地址翻译硬件正常处理了
      (Ref:CSAPP 3e p.564)
      7.9动态存储分配管理
      本节将简述动态内存分配管理的基本方法和策略。

7.9.1显式分配器
将空闲块组织为某种形式的显式数据结构不失为一种好方法。根据定义,程序不需要一个空闲块的主体,所以实现空闲链表数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。
使用双向链表而非隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于空闲链表中块的排序策略。
一种维护链表方法是用后进先出(LIFO)的顺序,将新释放的块放置在链表的开始处。这种情况下,不论是否使用了边界标记,合并可以在常数时间内完成。
另一种方法是按照地址顺序维护链表,其中链表中每个块的地址都小于它后继的地址。这种情况下,free一个block需要线性时间的搜索来定位合适的前驱。
7.9.2隐式分配器(边界标记)
边界,对应于Knuth提出的一种边界标记技术。允许在常数时间内进行对前面块的合并。这种思想是在每个块的结尾处添加一个脚部,其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。
一个块是由一个字的头部、有效载荷、可能的一些额外的填充,以及在块的结尾处的一个字的脚部组成的。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是0。因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息。在这种情况中,我们用其中的最低位(已分配位)来指明这个块是已分配的还是空闲的。
头部后面就是应用调用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。需要填充有很多原因。比如,填充可能是分配器策略的一部分,用来对付外部碎片。或者也需要用它来满足对齐要求。
将堆组织为一个连续的已分配块和空闲块的序列,例如:

图7-1 用隐式空闲链表组织堆。阴影部分表示已分配块。

称隐式空闲链表。因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。注意:此时我们需要某种特殊标记的结束块,可以是一个设置了已分配位而大小为零的终止头部。
7.9.3 已分配块、空闲块的对应策略
7.9.3.1已分配块
放置已分配的块:
a)首次适配:
从头开始搜索空闲链表,选择第一个合适的空闲块
b)下一次适配:
从上一次查询结束的地方开始(而非从链表起始开始)
c)最佳适配:
检查每个空闲块,选择适合所需请求大小的最小空闲块。
7.9.3.2 空闲块
1)合并空闲块
a)立即合并:
在每次一个块被释放时,就合并所有的相邻块
b)推迟合并:
等到某个稍晚的时候再合并空闲块。
2)分割空闲块
分配器通常会选择分割空闲块为两部分。一部分变成分配块,剩下一部分变成一个新的空闲块。

(Ref:CSAPP 3e p.594, 595)
7.10本章小结
本章结合hello说明了逻辑地址、线性地址、虚拟地址、物理地址的概念。叙述了hello存储器地址空间有关原理和概念。介绍了Intel逻辑地址到线性地址的变换方法和Hello的线性地址到物理地址变换方法。简述了段式管理、页式管理。叙述了VA-PA的变换,分别基于TLB和四级页表支持下的视角。讲述了三级Cache支持下的物理内存访问。hello进程调用fork时的内存映射和execve时的内存映射以及故障中断处理。简述了动态内存管理的基本方法与策略。动态内存分配器维护一个内存的虚拟区域。分配器有显式、隐式两种基本风格。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
(Ref:CSAPP 3e p.576)
8.2 简述Unix IO接口及其函数
8.2.1 Unix IO接口简要叙述
Unix IO接口是内核提供的一种简单的应用接口来实现文件相关操作。在这里,文件对应着着Linux以文件方式读写的IO设备。
8.2.2 Unix IO接口函数简要叙述
1)open()
功能:打开一个已经存在的文件或者创建一个新文件。
2)lseek()
功能:改变文件位置
3) read()
功能:从当前文件位置读取字节到内存。
4) write()
功能:从内存写入字节到当前文件位置。
5) close()
功能:关闭一个打开的文件。
8.3 printf的实现分析
 vsprintf函数接受格式字符串fmt,产生格式化输出,返回要打印的字符串的长度。
vsprintf函数总体实现:

图8-1 vsprintf函数的实现
 函数write传参给寄存器,sys_call被调用,寄存器中字符串字节- >总线- >显存
write函数汇编实现:

图8-2 write的汇编实现
 ASCII(存储在显存中) - > 字模库 - > vram 的display (子程序驱动)。vram将由显示芯片读取, RGB分量(每一点)经由信号线传向液晶显示器。

printf函数的总体实现如图:

图8-3 printf函数的总体实现
8.4 getchar的实现分析
根据getchar()的实现可以看明白,缓冲区数据被读入buf,若buf里数据长度不为0,getchar返回buf第一个元素。

图8-4 getchar()里的read调用和返回值

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

附getchar的库实现:
int getchar(void){
static char buf(SIZE_OF_BUF);
static char* bb = buf;
static int n = 0;
if(!n){
n = read(0, buf, SIZE_OF_BUF);
bb = buf;
}
return (–n >= 0) ? (unsigned char)*bb++ : EOF;
}
8.5本章小结
简述了Linux IO设备管理方法。Unix IO接口是内核提供的一种简单的应用接口来实现文件相关操作。在这里,文件对应着着Linux以文件方式读写的IO设备。Unix IO接口对应有五个简单的函数。解析了printf和getchar这两个C常用函数的具体机制。

结论
hello经历了如下生命历程:
它总体上是From 0 to 0的,
hello.c由用户在空白文本编辑界面编写代码而来,用户编写之前,是0的状态。
用户经过编辑器编写,从0得到hello.c,hello.c经过P2P(Program to Process)过程,
GCC C编译器ccl驱动程序读取源程序文本文件hello.c
预处理器(cpp)预处理, 得修改后的源程序文本文件hello.i
编译器(ccl) 编译,得汇编程序文本文件hello.s
汇编器(as)汇编,得可重定位目标程序(二进制)hello.o
链接器(ld)把hello.o和printf.o链接生成可执行目标程序(二进制)hello
壳(Bash)中,进程管理(OS)调用fork函数创建新子进程;
使用mmap把它映射至虚拟内存;
虚拟地址被TLB、Cache(3级)、页表(4级)转换为物理地址;
execve函数被调用加载可执行文件hello;
hello被分时操作系统分配时间片;
hello指令被取指译码执行
最终进入hello可执行目标程序的被执行阶段,在操作系统中以进程的形式运行,终止后被父进程回收,被内核从系统清除。
终止后在系统中被内核清除后,hello又回到了0的状态。

课程感悟:
计算机系统这门课是本学期安排最好的一门专业课,但也不乏上升空间。
首先,CSAPP无疑是计算机系统的经典教材。这本教材的质量是很高的,它没有像某些教材一样东拼西凑组成一本干巴巴的知识集合,而是通过详细讲解为什么学这个知识、为什么章节要这样分类、知识板块这样归纳的理由、知识板块之间的联系、本书的顶层设计理念和架构,阅读过程中似淳淳流淌的甘泉,流畅又不失趣味,时不时还有一些细小知识和历史。就是翻译感觉很机械和僵硬(ofc,这是翻译者的锅),不过瑕不掩瑜。感谢这门课为我们选择这个教材。
其次是各种lab的设计,做lab的过程能让我初步熟悉在Linux下工作的最基本技能,撰写实验报告时对操作、设计、知识的分析也巩固了我的理论知识。但是仍有上升空间,虽然无足轻重但最直观的感觉就是lab设计的老旧,比如PPT一直沿用以前的,时间、地点和一些说明脱节了,给学生印象不是很好。不过体验总体上还是不错的,尤其是第三、四章buffer,像走迷宫一样,很好地满足了学生的探索欲和用刚学的知识递进地解决问题的成就感。
最后是这个大作业。总体来说这个大作业我觉得很有帮助和意义,与其说是论文不如说是总结,但它并不是纯文字的总结,而是结合了实际操作。在做大作业的过程中我在总体视角上加深了对本学期知识板块之间关联的理解。

附件
hello.i:
hello.c的预处理后产生的文件。
hello.s:
hello.i的编译后产生的汇编问间。
hello.o:
hello.s的汇编后产生的可重定位文件。
hello.elf:
hello.o的elf格式文件
hello-ld.elf:
hello的elf格式文件
hello:
hello.o的链接后产生的可执行文件。

参考文献
[1] Randal E. Bryant & David R. O’Hallaron. 深入理解计算机系统[M]. 北京:机械工业出版社,2019:1-613.
[2] Pianistx.printf 函数实现的深入剖析.
https://www.cnblogs.com/pianist/p/3315801.html
[3] getchar()用法解释. https://ask.csdn.net/questions/226441
[4] getchar()
https://baike.baidu.com/item/getchar%28%29/6876946?fr=aladdin
[5] CSU_IceLee. Unix IO. https://www.jianshu.com/p/5228e027bbcb

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值