计算机系统大作业——程序人生

计算机系统

大作业

题 目 程序人生-Hello’s P2P

摘 要
本文梳理探讨了简单的C语言程序hello,在x86/Linux系统环境下,从源代码文件开始到最终完成执行完成的简单一生。其一生高度概括了程序在计算机系统体系结构中运行的各个重要阶段,包括编译处理、进程管理、内存管理、异常控制流、硬件存储结构层次、系统IO管理等。从一个简单程序作为切入点,深入探讨hello程序的完整执行过程,可以抽丝剥茧以小知大,更好的帮助我们深入理解计算机系统的结构,抽象出软硬件体系的运行管理机制。

关键词:计算机系统;软件;硬件;Linux;x86;

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

目 录

第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语言源代码文件Hello.c的形式存在于硬盘上的。经过预编译阶段将头部include调用文件包含到源代码中,并将源代码中的宏定义替换,形成hello.i文件。再将完整的代码交给编译器进行编译,得到hello.s的汇编文件。汇编器将汇编文件处理为二进制可重定位文件,之后再交给链接器进行链接,最终形成可执行文件hello.o。在Linux系统终端中运行可执行文件,内核使用系统调用fork为其创建一个新的进程,在该进程的上下文中使用系统调用execve执行hello.o文件,至此,hello程序以进程的形式运行在计算机中,完成了从程序到进程的转变。
O2O:From Zero-0 to Zero-0
意即在内存中从0化为0。在运行hello.o文件前,hello程序在内存中是没有任何痕迹的。当内核为运行hello程序fork出一个进程后,调用execve执行hello文件,此时内核为hello分配虚拟内存空间,删除当前虚拟内存空间中用户部分已存在的数据结构,为hello的代码段、数据、bss以及栈区域创建新的区域结构,并将hello的源代码、数据等信息复制到内存中。然后虚拟内存与磁盘对象建立映射,设置程序计数器,使之指向代码区域的入口点,程序入口后虚拟内存映射的内容开始载入物理内存。做好执行程序的准备后进入main函数,CPU为hello分配时间片,将控制交给hello进程,执行hello的程序内容。待执行完毕后,hello成为僵死进程,等待父进程的回收,回收后内核会从内存中删除hello的所有痕迹,hello在内存中重新化为0。至此hello完成了在内存中从无到有再到无的过程,即从0到0。
1.2 环境与工具
硬件环境:X64 CPU;1.8GHz;8G RAM;512G SSD
软件环境:VMware 15;Ubuntu 20.04 LTS 64位
开发工具:GDB;EDB;GCC;Linux内置指令

1.3 中间结果

图1.3.1 中间生成文件一览
hello.c C语言源代码文件
hello.i 预处理阶段生成文件
hello.s 编译阶段生成文件,内容为汇编代码
hello.o 汇编阶段生成文件,二进制可重定位目标文件
hello 可执行目标文件
hello.elf 可重定位目标文件的ELF格式
hello2.elf 可执行目标文件的ELF格式
hello_o_asm.txt 可重定位目标文件的反汇编代码
hello_asm.txt 可执行目标文件的返汇编代码
1.4 本章小结
对P2P和O2O在本文中的特殊含义进行了阐述,简要概括了hello程序的一生。本章还列出了下文中使用到的工具以及生成的文件,对下文起说明性作用。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
概念:
在C文件编译前对源代码进行的处理,主要包括文件包含、宏定义替换、条件编译等。C文件预处理后得到后缀为.i的预处理文件。
作用:
在hello程序中,预处理主要体现为头文件的包含,即将程序头部使用#include指令包含的文件复制整合到源代码中,生成hello.i文件方便后续编译。但在其他C文件中预处理常常还包括宏定义替换和条件编译。宏定义替换的作用是将程序中使用#define定义的部分在源代码中进行替换。条件编译的作用是根据源代码中的条件编译命令(如#if、#ifdef)选择性的编译部分源代码,方便程序的多样性实现。
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i

图2.2.1 预处理命令
生成的预处理文件部分内容如下所示:

图2.2.2 预处理文件部分内容

2.3 Hello的预处理结果解析

图2.3.1 预处理文件部分内容
生成的预处理文件名为hello.i,该文件共3065行,与hello.c源代码相比文件内容大大增加。
其文件底部为hello.c的main函数代码。上面3000余行可以看到stdio.h等标注,均为复制整合的包含的头文件,即<stdio.h> <unistd.h> <stdlib.h>的内容。这体现出预处理包含头文件的功能。
另外注意到预处理文件不含有任何宏定义,虽然hello源代码中并无宏定义,但包含的头文件源代码中普遍存在宏定义,在预处理文件中全部得以替换,也体现出预处理的宏定义替换功能。
2.4 本章小结
本章关注C程序的预处理阶段。阐述了预处理操作的概念和作用后,实际操作生成并查看了预处理文件,通过对比预处理文件和源代码文件,对于预处理操作的实际功能有了更为具体的认识。
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
概念:
编译指C语言代码转化为汇编指令的过程,具体体现为编译器将预处理文件转化为以.s为后缀的汇编文件。
作用:
编译器将源代码翻译为汇编指令的过程分为五个阶段:词法分析,语法分析,代码优化(中间代码生成),目标代码生成。
语法语义分析阶段的主要作用是检查语法错误,并给出错误提示信息。
若代码无语法错误,则进入下一阶段代码优化阶段,此阶段的主要作用是根据程序功能对代码进行等价变换,使得程序的算法实现更加高效合理,此阶段产生的优化代码称为中间代码。
最后进入目标代码生成阶段,编译器将优化后的中间代码转化为目标代码,即汇编指令代码。
3.2 在Ubuntu下编译的命令
gcc -S hello.c -o hello.s

图3.2.1 编译命令
生成的汇编文件部分内容如下图所示:

图3.2.2 汇编文件部分内容

3.3 Hello的编译结果解析
3.3.1 数据类型
Hello中涉及到的变量有全局整型变量sleepsecs、整型变量argc、字符串数组argv、局部整型变量i。

图3.3.1 汇编代码内容
sleepsecs作为已初始化的只读全局变量被存放在rodata段。

图3.3.2 汇编代码内容
参数argc和argv分别被存放在寄存器%edi和%rsi中被传入,随后被压入栈。对argc的赋值操作使用的后缀是l,对应4字节的int类型,对argv的赋值操作后缀为q,对应8字节的字符指针类型。

图3.3.3 汇编代码内容
局部变量i作为循环控制变量,在汇编代码中被存放在栈中。操作后缀为l,对应4字节的int类型。

图3.3.4 汇编代码内容
源代码中printf函数打印的内容被当作字符串常量存储rodata段。其中第一个字符串的内容涉及到中文字符,在汇编代码中使用utf-8编码表示。
其余数据以立即数的形式给出,直接编码在汇编代码中。
3.3.2 赋值
源代码中涉及到的赋值操作为局部变量i的初始化i:=0,在汇编代码中体现为

图3.3.5 汇编代码内容
Mov S,D操作表示将S的值传送给D。在实际应用中,mov指令跟随后缀,上述代码的后缀为l,表示执行的是4字节操作,对应i变量所属的int类型。0以立即数的形式给出,i变量存放在栈中%rbp-4所指的位置,故通过movl直接赋值即可。
汇编代码中其余的赋值操作均为代码翻译时产生的操作,mov后跟随的后缀有l、q,分别代表4字节、8字节操作。操作数有立即数、寄存器、寻址形式等。
3.3.3 Sizeof
Sizeof是C语言中的一个一元操作符,返回对象或类型在内存中所占的字节数。Hello程序中涉及到的变量类型有int、char*,执行sizeof操作返回值分别为4和8。
3.3.4 算术和逻辑操作
Hello程序中涉及到的算术逻辑操作主要是加法操作,即add。汇编代码中还出现了leaq加载有效地址指令。
除此之外,常用的算数逻辑操作还包括减sub、乘imul、异或xor、或or、与|、取负neg、取补not、左移sal、算术右移sar、逻辑右移shr等。操作数个数视具体操作而定。操作后同样可以添加后缀以指明数据格式。
3.3.5 关系操作
关系操作主要包含在比较测试指令cmp、test和跳转指令中。

图3.3.6 汇编代码内容
源代码中涉及到的argc!=3判断和循环判断i<10均通过比较指令来设置条件码,而后使用条件跳转指令根据条件码来改变程序控制。

图3.3.7 汇编代码内容
Cmp S1,S2基于S2-S1的值设置条件码。例如S2-S1=0时je会读取到条件码设置为相等,此时执行跳转操作,否则不执行跳转。对于jle指令,如果读取到当前条件码为小于等于那么执行跳转,否则不执行。
同样对于比较测试指令可以设定后缀来指明操作的数据格式。
3.3.6 数组/指针操作
C语言中数组往往占据一片连续的内存区域,故使用数组开头的指针即可指代整个数组。Hello程序中argv即为字符指针,它存储一个字符数组的开头地址,即可指代一个字符数组。

图3.3.8 汇编代码内容
汇编代码中仅将数组头指针压入栈,若想对数组执行索引取值操作,只需以头指针为基准,根据数据格式和索引设置偏移量,即可寻址取得所需的数值。

图3.3.9 汇编代码内容
如对argv[1]和argv[2]的寻址。栈中%rbp-32处存放数组头指针,字符指针类型占据8字节,故一位索引偏移为8,两位索引偏移为16。汇编代码中根据索引和数据格式设置偏移量后,直接使用mov操作寻址赋值即可获取所需的数值。
3.3.7 控制转移
控制转移基于跳转指令。Hello程序中涉及的控制转移为if(argc!=3)和for循环引起的跳转,汇编代码中对应的跳转指令如下。

图3.3.10 汇编代码内容
上述为条件跳转指令,即指令根据条件码的设置情况有选择的进行跳转。Je表示条件码为相等时跳转,jle表示条件码为小于等于时跳转。除此之外jmp指令不依据条件码执行直接跳转操作。
3.3.8 函数操作
函数是一种过程,假设过程P调用过程Q吗Q执行后返回到P,这些动作包含限免一个或多个机制:
1.传递控制:进行过程 Q 的时候,程序计数器必须设置为 Q 的代码的起始 地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的 地址。
2.传递数据:P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P 中返回 一个值。
3.分配和释放内存:在开始时,Q 可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。
过程调用使用call指令。过程调用与栈结构的使用关系密切,在执行call指令前需要先传递参数,将调用返回地址压入栈中,而后设置PC将控制交给调用过程。每一层调用都有属于自己的栈帧,维护栈帧是保证调用顺利运行的关键,若栈帧遭到破坏则会导致严重的恶性错误。
Hello程序中使用的调用有main函数、printf函数、exit函数、sleep函数、getchar函数。其中main函数由系统启动函数调用。每一次函数调用前都会执行传递参数,返回地址压栈,PC设置等步骤,每一层函数调用都由属于自己的栈帧。
3.4 本章小结
本章详细分析了hello程序的汇编代码。通过分析比较汇编代码与对应源代码之间的关系和区别,了解编译的基本机制和原理,对编译过程有了深层次的理解,对程序的机器级表示有了更好的领悟。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
概念:
汇编程序被翻译为机器语言指令的过程。具体表现为汇编器(as)将后缀为.s的汇编文件打包翻译成可重定位目标程序的格式,并将结果保存在.o目标文件中。.o文件是一个二进制文件,它包含程序的机器指令编码。
作用:
生成可重定位的目标文件。
4.2 在Ubuntu下汇编的命令
gcc -c hello.c -o hello.o

图4.2.1 汇编命令
生成的可重定位目标文件如下:

图4.2.2 生成文件
4.3 可重定位目标elf格式
获取 ELF 格式指令:readelf -a hello.o > hello.elf

图4.3.1 ELF头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小与字节顺序。ELF头剩下的部分包括ELF头的大小、目标文件的类型、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。

图4.3.2 ELF节头部表
节头部表概述了文件中出现的各个节的基本信息,包括节的名称、大小、类型、地址、偏移量等。

图4.3.4 ELF重定位节
Hello的重定位节包括rela.text节、rela.eh_frame节、symtab节。重定位节中各信息的表示含义如下。
偏移量:需要被修改的引用的节的偏移。
信息:包括 symbol 和 type 两部分, 其中 symbol 占前 4 个字节, type占后 4 个字节,symbol 代表重定位到的目标在.symtab中的偏移量,type 代表重定位的类型。
类型:告知链接器如何修改新的引用。
两种最基本的重定位类型:
R_X86_64_PC32 :重定位一个使用32位PC相对地址的引用。
R_X86_64_32 :重定位一个使用32位PC绝对地址的引用。
符号名称:重定位目标的名称。
加数:一个有符号常数,一些类型的重定位要使用它对被修改引用的值做调整。
rela.text节是一个text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。例如hello程序中的puts函数、exit函数、printf函数、sleep函数。这些函数均为外部调用函数。而sleepsecs为全局变量引用,其被存放在.data段,引用时的位置需要修改。
Symtab节是一个符号表,它存放在程序中定义和引用的函数和全局变量信息。每个可重定位目标文件在symtab中都有一张符号表,但symtab符号表中不含局部变量的条目。Hello的symtab节中包含了程序中引用的函数如printf、exit、sleep、getchar,以及引用的全局变量sleepsecs。
4.4 Hello.o的结果解析
在Linux终端执行objdump -d -r hello.o指令,在终端中获取hello.o的反汇编代码如下图所示。

图4.4.1 反汇编代码
将反汇编代码与hello.s中的汇编代码进行比较,可以得到如下差别:
反汇编代码指明了每一条指令的16进制机器码以及所处的地址。
反汇编代码中操作数均为16进制,而汇编代码中为10进制。
反汇编代码中部分指令省略了后缀的数据格式表示,汇编代码中均带有后缀。
反汇编代码中涉及到的控制转移,如跳转操作和函数调用,其操作数均为目标指令的地址,而汇编代码中操作数是段名称或函数名称。最重要的是,反汇编代码中函数调用callq后的操作数地址指向下一条指令的地址,而不是特定函数的地址。这是因为hello中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text节中为其添加重定位条目,等待静态链接的进一步确定。
反汇编代码中对于全局变量的引用为%rip+0。这是因为rodata中数据地址也是在运行时确定,故访问需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。
综上来看,反汇编代码除在格式上与汇编文件不同外,其指令着重体现出文件的可重定位特征。
4.5 本章小结
本章通过分析可重定位文件的ELF文件和反汇编代码内容,得到了二进制的可重定位目标文件的特征,进而理解了汇编器在程序编译处理过程中扮演的角色。可重定位文件表现的重定位特征也为接下来的链接阶段做了铺垫。
(第4章1分)

第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.2.1 链接指令
生成的文件如图所示:

图5.2.2 链接生成文件
5.3 可执行目标文件hello的格式
生成ELF格式指令为readelf -a hello > hello2.elf

图5.3.1 ELF格式指令

图5.3.2 ELF文件头
可执行目标文件的ELF文件头内容如上图所示,其与可重定位目标文件的ELF头相似,除ELF文件节条目改变外,最主要的区别是可执行目标文件ELF头有了程序入口点地址,即程序运行时PC需设置的第一条指令地址。

图5.3.3 ELF文件节头部表部分内容
可执行目标文件的ELF格式中节头部表条目增多,对 hello 中所有的节信息进行了声明。各个节在内存中的虚拟地址和大小有了明确标明,可以根据虚拟地址信息定位各个节所占的区间。
可执行目标文件ELF格式的明显特征是拥有了程序头,且不再有rel.text节和rel.data节,这些节中的可重定位信息已被重定位到最终的虚拟内存地址。但存在一些重定位节如.rela.plt和.rela.dyn,这些节需要之后进行动态链接。

图5.3.4 ELF文件中存在的重定位节
5.4 hello的虚拟地址空间
使用edb打开hello的可执行目标文件,如下图所示。

图5.4.1 edb打开hello文件
Edb左上窗口展示了汇编代码对应的16进制机器码和其所处的地址信息。

图5.4.1 虚拟空间范围
在Data Dump窗口可以看到hello文件加载的虚拟空间范围为0x401000-
0x402000.

图5.4.1 edb中查看各节地址
使用symbol窗口可以查看文件各节在虚拟地址空间中的地址,其地址信息与ELF格式中节头部表中的信息完全一致。
5.5 链接的重定位过程分析
执行objdump -d -r hello指令得到hello的反汇编代码如下图所示。

图5.5.1 hello的部分反汇编代码

对比第四章hello.o的反汇编代码,得到以下区别:
1.在hello.o反汇编中,main函数地址从0开始,即地址指代均为相对偏移地址;而在hello中main函数0x4010c1开始,故hello已将hello.o重定位为了虚拟内存地址,
2.hello可执行目标文件中增加了.init节和.plt段。.init节定义了一个_init函数,程序的初始化代码会调用它,用于初始化程序执行环境;.plt段是程序执行时的动态链接。并且所有重定位条目都被修改为了确定的运行时虚拟内存地址。
3.将hello.c中用到的函数,如exit、printf、sleep、getchar等,链接到了hello中,在反汇编代码中可以看到为上述函数分配的虚拟内存空间信息。
链接就是链接器ld将.o文件中的分散在外部文件中的目标函数段按照一定规则抓取组合到hello文件的过程。指定动态链接器为/lib64/ld-linux-x86-64.so.2。外部目标文件crt1.o、crti.o、crtn.o中主要定义了程序入口_start、初始化函数_init等运行程序必须的函数。ibc.so是动态链接共享库,其中定义了hello.c中用到的printf、sleep、getchar、exit函数。链接指令ld即是将上述文件和函数抓取并组合到hello文件中。
重定位由两步组成:重定位节与符号定义、重定位节中的符号引用。
在重定位节与符号定义这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节,而后,链接器将运行时虚拟内存地址赋值给新的聚合节、输入模块定义的每个节,以及输入模块定义的每个符号。
在重定位节中的符号引用中,链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行时虚拟地址,这一步依赖hello.o中的重定位条目。
除此之外重定位类型分为两种,分别为R_X86_64_PC32与R_X886_64_32,这两种分别为PC相对寻址与绝对寻址。对于hello.o中使用PC相对寻址的指令使用R_X86_64_PC32类型进行重定位,而对hello.o直接引用地址的指令,采用R_X886_64_32类型进行重定位。
5.6 hello的执行流程
执行过程中调用的所有函数如下:
ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
hello!_start
libc-2.27.so!__libc_start_main
-libc-2.27.so!__cxa_atexit
-libc-2.27.so!__libc_csu_init
hello!_init
libc-2.27.so!_setjmp
-libc-2.27.so!_sigsetjmp
-libc-2.27.so!__sigjmp_save
hello!main
hello!puts@plt
hello!exit@plt
hello!printf@plt
hello!atoi@plt
hello!sleep@plt
hello!getchar@plt
ld-2.27.so!_dl_runtime_resolve_xsave -ld-2.27.so!_dl_fixup
-ld-2.27.so!_dl_lookup_symbol_x
libc-2.27.so!exit
5.7 Hello的动态链接分析

图5.7.1 dl_init之前.got.plt节的内容

图5.7.2 dl_init之后.got.plt节的内容
使用edb调试,Data Dump窗口数据的值在运行dl_init前后发生变化。如图地址0x404000处的值改变,根据ELF文件,此处为.got.plt节。
动态链接库是在进程启动时加载进来的。加载后,动态链接器需要对其作一系列的初始化,如符号重定位(动态库内以及可执行文件内),这些工作是比较费时的,特别是对函数的重定位。因此为了节约时间对函数的重定位延迟进行,这使得我们可以对一些动态库里包含的很多全局函数之中的很小一部分我们使用到的、执行到的进行重定位。具体来说,就是应该等到第一次发生对该函数的调用时才进行符号绑定,也就是延迟绑定。
延迟绑定的实现步骤有:

1.建立一个.got.plt表,该表用来放全局函数的实际地址,但最开始时,里面放的不是真实的地址而是一个跳转。
2.对每一个全局函数,链接器生成一个与之相对应的影子函数。
而所有对函数的调用,都换成对{(函数名)@plt} 的调用。而{(函数名)@plt}的第一条指令会直接从 got.plt 拿真实的函数地址,如果之前已经发生过调用,got.plt 就已经保存了真实的地址,如果是第一次调用,则 got.plt 中放的是 {(函数名)@plt} 中的第二条指令,这就使得当执行第一次调用时, {(函数名)@plt}中的第一条指令其实什么事也没做。
继续往下执行,第二条指令的作用是把当前要调用的函数在 got.plt 中的编号传给_init(),而_init()将被调用函数进行重定位,然后把结果写入到 got.plt 相应的地方。
5.8 本章小结
本章关注可重定位目标文件到可执行目标文件的过程。这个过程包括两个重要过程:链接、重定位。通过分析ELF文件格式、借用edb查看虚拟内存与代码间的映射关系,我们深入理解了程序链接和重定位过程中的机制和发生的变化。至此,hello程序从易于人理解的C语言代码彻底转变为了易于机器读懂的二进制文件了。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程的经典定义是一个执行中程序的实例。系统中每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容,程序计数器、环境变量以及打开文件描述符的集合。
作用:
它提供一个假象,好像我们的程序是系统当前运行的唯一的程序一样。我们的程序好像是独占的使用处理器和内存。处理器好像无间断的一条接一条的执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell是一个交互型应用级程序,它代表用户运行其它程序。Shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,代表用户运行程序。
处理流程:
1.解析以空格分割的命令行代码,并构造最终会传递给execve的argv向量。
2.解析了命令行之后,检查第一个命令行参数是否是一个内置的shell命令。如果,它立即解释该指令。
3.如果不是内置指令,那么shell创建一个子进程,并在子进程中执行所请求的程序。
4.如果用户要求在后台运行该程序,那么shell等待下一个命令并返回步骤1。
5.如果不要求在后台运行,那么shell使用waitpid等待作业终止。待终止后,shell会等待下一个命令并返回步骤1。
6.3 Hello的fork进程创建过程
Shell检测到命令行第一个参数不是内置指令后,通过调用fork函数创建一个新的运行的子进程来运行hello。
新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得父进程打开任何文件描述符相同的副本,这意味着当父进程调用fork函数时,子进程可以读写父进程中任何打开的文件。父进程与子进程之间最大的区别在于它们拥有不同的 PID。

图6.3.1 fork进程过程
6.4 Hello的execve过程

图6.4.1 新程序开始时用户栈典型的组织结构
Execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。
当加载器运行时,它将创建内存映像(在Linux x86-64系统中,代码段总是从地址0x40000处开始)。而在程序头部表的引导下,加载器将可执行文件的片复制到代码段和数据段。接下来,加载器跳转到程序的入口点,即_start函数的地址,_start函数调用系统启动函数 _libc_start_main来初始化执行环境,并调用用户层的main函数,此时构造的argv向量被传递给主函数。
在execve加载了hello之后,它调用启动代码设置栈,并将控制传递给新程序的主函数。主函数有以下原型:
Int main ( int argc, char *argv[], char *envp[] );
6.5 Hello的进程执行
为了使操作系统内核提供一个无懈可击的进程抽象,处理器必须提供一种机制, 限制一个应用可以执行的指令以及它可以访问的地址空间范围。处理器通常使用某个控制寄存器的一个模式位提供两种模式的区分,该寄存器描述了进程当前享 有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
内核会为每一个进程维持一个上下文,而在进程执行的某些时刻,内核可以进行调度、抢占当前进程,并重新开始一个先前被抢占了的进程。上下文切换的流程是1.保存当前进程上下文。2.恢复某个先前被抢占的进程被保存的上下文。3.将控制传递给这个新恢复的进程。

图6.5.1 进程上下文切换
控制传递到hello的入口处后,运行时内核为其保存一个上下文,进程在用户模式运行。如果没有异常或中断信号的产生,hello将继续正常地执行。如果遇到了异常或者中断信号,那么进程陷入内核模式,内核执行上下文切换,将控制转换到其它进程。当执行sleep函数时,hello进程将挂起指定时间,此时发生上下文切换,进程陷入内核模式,控制转移至另一个进程;当达到指定挂起时长后,再次发生上下文切换,进程陷入内核模式,将控制转移到原先挂起的hello进程。当hello循环结束后,调用getchar函数,需要键盘缓冲区DMA传输,发生异常,进程进入陷阱陷入内核,执行上下文切换,将控制转移给其他进程完成键盘输入。DMA传输完成后,内核切换回 hello 进程,然后hello执行return,进程终止。
6.6 hello的异常与信号处理

图6.6.1 正常运行结果
程序正常运行的结果如上。

图6.6.2 任意输入运行结果
程序运行过程中任意输入不会影响程序的运行,仅会在终端中显示输入的字符。程序最终的getchar函数会读取键盘输入的第一个值。

图6.6.3 输入Ctrl-Z运行结果
在程序运行过程中输入ctrl-z之后,shell 父进程接收到 SIGSTP 信号,将hello进程暂时挂起。通过ps命令观察,可以hello进程并未被回收。

图6.6.4 使用ps指令观察当前进程
再运行一个新的hello进程,并使用ctrl+Z挂起,在此之后输入fg指令,将hello进程调回前台执行,可以看到第二个hello进程又在之前的挂起处继续执行。

图6.6.5 使用fg指恢复前台进程
再次使用Ctrl-Z将进程挂起,使用jobs查看当前作业,可以看到先前运行的两个hello进程。

图6.6.6 当前作业
再次恢复第二个hello进程,输入Ctrl-C,此时第二个进程接收到SIGINT信号,接到来自键盘的终止,故转到信号处理程序,终止该进程。再次使用jobs查看当前作业,可以看到仅剩第一个hello进程。

图6.6.7 使用Ctrl-C指令
再使用kill命令根据第一个进程的PID杀死进程。进程接收到SIGKILL信号,进程被杀死。再次使用ps查看当前进程,已没有hello进程在执行。

图6.6.8 使用kill指令后的进程表
综上,经过上述各种指令操作,进程收到了三种信号:SIGINT来自键盘的终止、SIGSTP挂起进程、SIGKILL杀死进程。
6.7本章小结
本章详细分析hello进程在系统上运行的过程。通过在hello实例进程运行过程中施加各种操作来体现进程运行中的异常控制流、信号处理。同时还探讨了有关进程管理、上下文切换的机制和原理。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址即为程序产生的与段相关的偏移地址,也叫相对地址。逻辑地址由段基值和偏移量组成,要经过寻址方式的计算或变换才得到内存储器中的实际有效物理地址。Hello.o文件中的地址即为逻辑地址。
线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。即为hello中的虚拟地址。
虚拟地址:程序使用的地址,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址被送到内存之前通过硬件与操作系统被转换成物理地址。Hello可执行文件反汇编代码中的地址即为虚拟地址。
物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址,又叫实际地址或绝对地址。Hello在运行过程中虚拟地址通过MMU映射得到物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在实地址模式下,处理器使用20位的地址总线,可以访问1MB(0~FFFFF)内存。而8086的模式,只有16位的地址线,不能直接表示20位的地址,采用内存分段的解决方法。段地址存放于16位的段寄存器中(CS、DS、ES或SS)7.3 Hello的线性地址到物理地址的变换-页式管理。
在保护模式下,段寄存器存放段描述符在段描述符表中的索引值,称为段选择器,此时CS存放代码段描述符的索引值,DS存放数据段描述符的索引值,SS存放堆栈段描述符的索引值。
48位的全局描述符表寄存器GDTR指向GDT,即GDT在内存中的具体位置,16位局部描述符表寄存器LDTR指向LDT段在GDT中的位置。唯一的全局描述符表GDT包含操作系统使用的代码段、数据段、堆栈段的描述符,各程序的LDT段,每个程序有一个独立的局部描述符表LDT,包含对应程序私有的代码段、数据段、堆栈段的描述符、对应程序使用的门描述符:任务门、调用门等。
7.3 Hello的线性地址到物理地址的变换-页式管理
概念上而言,虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每个字节都有一个唯一的虚拟地址,作为数组的索引。磁盘上数组的内容被缓存在主存中。而VM系统将虚拟内存分割为称为虚拟页的固定大小的块,物理内存也同样被分割成物理页,物理页也被成为页帧。
在任意时刻,虚拟页面的集合都分为三个不相交的子集:未分配的、缓存的、为缓存的。

图7.3.1 物理页和虚拟页
物理内存中存放着一个叫页表的数据结构,页表是一个页条目表,将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。

图7.3.2 页表
DRAM缓存不命中成为缺页。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页。在磁盘和内存之间传送页的活动叫做交换或者页面调度。页从磁盘换入DRAM和DRAM换出磁盘。一直等待,直到最后时刻,也就是当有不命中发生时,才换入页面的这种策略称为按需页面调度。
7.4 TLB与四级页表支持下的VA到PA的变换
下图展示了MMU如何利用页表来实现虚拟地址空间和物理地址空间的映射。

图7.4.1 使用页表的地址翻译
每次CPU产生一个虚拟地址,MMU就必须查阅一个PTE,以便将虚拟地址翻译为物理地址。然而硬件系统中往往在MMU中包含了一个关于PTE的小的缓存,称为翻译后备缓冲器(TLB)。

图7.4.2 虚拟地址中用于访问TLB的组成部分
TLB是一个小的、虚拟地址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常由高度的相联度。用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。
页表驻留在内存中的体积过大会使问题变得复杂,用来压缩页表的常用方法是使用层次结构的页表。
例如,一级页表中的每个PTE负责映射虚拟地址空间中一个4MB的片,这里每一个片都是由1024个连续的页面组成的。
二级页表中的每个PTE都负责映射一个4KB的虚拟内存页面,就像我们查看只有一级的页表一样。

图7.4.3 一个两级页表层次结构
这种方法从两个方面减少了内存要求。第一,如果一级页表中的一个PTE是空的,那么相应的二级页表就根本不会存在。这代表着一种巨大的潜在节约。第二,只有一级页表才需要总是在主存中的;虚拟内存系统可以在需要时创建、页面调入或调出二级页表,这样就减少了主存的压力;只有最经常使用的二级页表才需要缓存在主存中。
7.5 三级Cache支持下的物理内存访问
以Core i7内存系统为例,处理器封装包括四个核、一个大的所有核共享的L3高速缓存,以及一个DDR3内存控制器。每个核包含一个层次结构的TLB、一个层次结构的数据和指令高速缓存,以及一组快速的点到点链路。TLB是虚拟寻址的,是4路组相联的。L1、L2、L3高速缓存是物理寻址的,块大小为64字节。L1和L2是8路组相联的,而L3是16路组相联的。
当CPU请求访问的虚拟地址VA被翻译为物理地址PA后,高速缓存根据组索引CI找到缓存组,在缓存组中根据标记CT与缓存行中的标记位匹配。如果匹配成功且有效位为1,则命中,按照块偏移CO访问指定数据。否则不命中,向下一级缓存中请求数据。如果下一级缓存中已缓存所需数据,那么按替换策略决定本级缓存的牺牲快进行替换,否则继续向下一级存储中寻找数据。

图7.5.1 Core i7地址翻译的概况
7.6 hello进程fork时的内存映射
当fork函数被shell进程调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在hello中返回时,hello进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。在这两个进程中的任一个后来进行写操作时,写时赋值机制就会创建新页面。因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地代替了当前程序,加载并运行hello程序需要以下几个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。
3.映射共享区域。hello文件与共享对象链接,这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器PC。Execve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需求换入代码和数据页面。
7.8 缺页故障与缺页中断处理
DRAM缓存不命中成为缺页。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页。
若CPU引用了一个页中的字,而所需页并未缓存在DRAM中,请求不命中,那么就会触发一个缺页异常。之后缺页处理程序选择一个牺牲页将所需的页换入,更新页表,随后返回。异常返回后会重新回到导致缺页的命令,指令会重新将虚拟地址发送给MMU,此时所需页已缓存在DRAM中,请求命中。
在磁盘和内存之间传送页的活动叫做交换或者页面调度。页从磁盘换入DRAM和从DRAM换出磁盘。一直等待,直到最后时刻,也就是当有不命中发生时,才换入页面的这种策略称为按需页面调度。也可以采用其他方法,例如尝试预测不命中,在页面实际被引用之前就换入页面。然而,所有现代系统都使用的是按需页面调度方式。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显示地保留为供应用程序使用。空闲块可用来分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格,两种风格都要求应用显式的分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
显式分配器,要求应用显式地释放任何已分配的块。
隐式分配器,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。
一个内存块是由一个字的头部、有效载荷,以及可能的一些额外的填充组成的。头部编码了这个块的大小,以及这个块是已分配块还是空闲的。由于块大小是8字节对齐的,故块大小最低3位总为0。因此我们用其中的最低位作分配位来指明这个块是已分配的还是空闲的。头部后面就是应用调用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。

图7.9.1 堆块的格式

在每个块的结尾处添加一个脚部,其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。
堆块的组织形式需要某种数据结构。常用的结构有隐式空闲链表、显式空闲链表和红黑树。其实现的吞吐率依次增加。
放置已分配的块时需要搜索空闲链表寻找一个足够大的位置放置块。这种搜索方式是由放置策略决定的。常见的策略有首次适配、下一次适配和最佳适配。首次适配的应用最为广泛,但缺点时容易留下小的空闲碎片。下一次适配可以提高吞吐率,但内存利用率往往要低得多。最佳适配拥有最高的内存利用率,但相应的吞吐率较低。
7.10本章小结
本章着重探讨虚拟地址空间与物理地址空间相互映射变换的关系,以及虚拟地址到物理地址的寻址方式,回顾指明了进程管理中fork和execve函数完成的具体工作,并分析了动态内存分配管理的策略。内存管理是程序在计算机上运行至关重要的一环,高效的内存管理方式将大大提升程序的运行效率和系统的安全性,虚拟地址概念以及虚拟地址寻址技术的产生是计算机科学发展的重要成就之一。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:Unix I/O接口
一个Linux文件就是一个m个字节的序列:
B0 , B1 , … , Bk , … , Bm-1
所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix I/O 接口:
1.打开文件:一个应用程序通过要求内核打开文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,用于标识这个文件。程序在只要记录这个描述符便能记录打开文件的所有信息。
2. shell 在创建的每个进程的开始时都为其打开三个文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。
3.改变当前文件的位置:对于每个打开的文件,内核保存着一个文件位置 k,初始为 0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行 seek 操作显式地设置文件的当前位置为 k。
4.读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文件位置 k 开始,然后将 k 增加到 k+n。给定一个大小为 m 字节的文件, 当 k>=m 时执行读操作会出发一个称为 EOF 的条件,应用程序能检测到这个条件,在文件结尾处并没有明确的 EOF 符号。
5. 关闭文件:内核释放打开文件时创建的数据结构以及占用的内存资源, 并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O 函数:
int open(const char *pathname,int flags,int perms)
功能:打开一个已存在的文件或者创建一个新文件。Open函数将filename转换为一个文件描述符,并且返回描述符数字。
int close(int fd)
功能:关闭一个打开的文件。关闭一个已关闭的描述符会出错。
ssize_t read(int fd, void *buf, size_t count);
功能:从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则返回值表示的是实际传送的字节数量。
ssize_t write(int fd, void *buf, size_t count);
功能:从内存位置 buf 复制至多n个字节到描述符fd的当前文件位置。
lseek();
功能:应用程序能够显示地修改当前文件的位置。
8.3 printf的实现分析
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; 
} 

va_list的定义:
typedef char va_list
这说明它是一个字符指针。
其中的 (char
)(&fmt) + 4) 表示的是参数fmt后的第一个参数。
vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。 它最终会返回一个长度,即要打印出来的字符串的长度。
从vsprintf生成显示信息,之后调用write系统函数陷入到陷阱-系统调用int 0x80或syscall。之后字符显示驱动子程序实现从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)的过程。最后显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
Getchar函数的源代码如下:
int getchar(void)
{
static char buf[BUFSIZ];
static char* bb=buf;
static int n=0;
if(n==0)
{
n=read(0,buf,BUFSIZ);
bb=buf;
}
return(–n>=0)?(unsigned char)*bb++:EOF;
}
在需要从键盘读入一个字符时,内核接收到异步异常,控制交给键盘中断处理程序。中断处理子程序接受按键扫描码转成ascii码,并保存到系统的键盘缓冲区。getchar函数中调用系统函数read,通过系统调用读取保存在系统键盘缓冲区的ascii码值,直到接收到回车键才返回。若用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中。
8.5本章小结
本章探讨hello程序的系统I/O管理,主要体现为printf函数和getchar函数的实现。通过对printf函数和getchar函数源代码的分析,更加深入理解了hello与外接设备的交互,并最终将信息打印到屏幕上的过程。
(第8章1分)

结论
Hello的一生可以概括为如下过程:
1.从编码阶段开始,hello程序以源代码的形式产生于硬盘上。这是一名入门程序员唯一参与的过程。
2.hello经过预处理、编译、汇编、链接、重定位转变为可执行目标文件。至此hello由易于人理解的C语言代码转变为机器可以理解的二进制代码。
3.hello通过操作系统内核的管理,以进程的形式运行于系统之上。
4.hello在运行过程中接收外部来自键盘的指令,并做出相应的反应。
5.hello运行完成,结束短暂的一生,留下的僵死进程由内核回收。至此hello在系统中运行的痕迹被全部抹除。
作为每一名程序员的入门程序hello,其实现如此简单而通俗易懂,以至于每个新手都会对它不屑一顾。实际上,hello的一生是短暂而精炼的。程序员们仅仅参与了代码的编写工作,hello余下的生命周期全部是在程序员不可参与的情况下执行的,而这些部分恰恰浓缩了现代计算机科学发展的成果,凝聚了一代代计算机科学家的心血和努力。为实现将姓名学号打印到电脑屏幕这一功能,看似简单的实现,其背后硬件系统与软件系统之间进行的精密、严谨而复杂的配合过程令人拍案叫绝。简单的hello程序凝聚的是人类智慧与科学发展的结晶,更是当今复杂程序系统,如操作系统、服务器、大型游戏、人工智能等实现的基石。
能够理解如此庞大复杂的体系和精妙神奇的机制是令人振奋的。然而计算机科学的发展仅仅经历了不到百年的时光,如今计算机科学仍在以迅猛的势头不断发展。在未来,量子计算机、人工智能、虚拟现实技术等等充满无限可能的科技正慢慢向我们靠近。我们有理由相信,计算机科学仍然有无穷的潜力去创造一个又一个令人类骄傲的奇迹!

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

附件
hello.c C语言源代码文件
hello.i 预处理阶段生成文件
hello.s 编译阶段生成文件,内容为汇编代码
hello.o 汇编阶段生成文件,二进制可重定位目标文件
hello 可执行目标文件
hello.elf 可重定位目标文件的ELF格式
hello2.elf 可执行目标文件的ELF格式
hello_o_asm.txt 可重定位目标文件的反汇编代码
hello_asm.txt 可执行目标文件的返汇编代码

(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 深入理解计算机系统 Randal E.Bryant David R.O’Hallaron 机械工业出版社
[2] printf函数实现的深入剖析 Pianistx 博客园 https://www.cnblogs.com/pianist/p/3315801.html
(参考文献0分,缺失 -1分)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值