计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 1190300505
班 级 1903011
学 生 张晟哲
指 导 教 师 史先俊
计算机科学与技术学院
2021年5月
本文旨在展示hello.c在Linux系统下执行的生命周期,探索它在计算机系统中经过预处理、编译、汇编、链接生成可执行文件的全过程,并研究在这一过程中计算机系统是如何对jello.c进行进程管理、存储分配和I/O处理的。通过对hello.c这一最原始的程序的生命周期的研究,我们会对计算机系统的各种工作原理有更深、更具体的认识和理解。
关键词:hello.c,预处理,编译,汇编,链接,进程管理,存储,虚拟内存分配,系统级I/O。
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 5 -
3.2 在Ubuntu下编译的命令............................................................................. - 7 -
4.2 在Ubuntu下汇编的命令........................................................................... - 12 -
5.2 在Ubuntu下链接的命令........................................................................... - 18 -
5.3 可执行目标文件hello的格式.................................................................. - 18 -
5.5 链接的重定位过程分析............................................................................... - 22 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 29 -
6.3 Hello的fork进程创建过程..................................................................... - 30 -
6.6 hello的异常与信号处理............................................................................ - 32 -
第7章 hello的存储管理............................................................................... - 35 -
7.1 hello的存储器地址空间............................................................................ - 35 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................ - 35 -
7.3 Hello的线性地址到物理地址的变换-页式管理...................................... - 36 -
7.4 TLB与四级页表支持下的VA到PA的变换............................................. - 37 -
7.5 三级Cache支持下的物理内存访问.......................................................... - 38 -
7.6 hello进程fork时的内存映射.................................................................. - 38 -
7.7 hello进程execve时的内存映射.............................................................. - 39 -
7.8 缺页故障与缺页中断处理........................................................................... - 39 -
8.1 Linux的IO设备管理方法.......................................................................... - 42 -
8.2 简述Unix IO接口及其函数....................................................................... - 42 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统术语,简述Hello的P2P,020的全过程:
P2P——在Linux中,hello.c经过cpp的预处理、ccl的编译、as的汇编、ld的链接最终成为可执行目标程序hello,在shell中输入启动命令后,shell为其fork产生一个子进程,使得hello从程序变成了一个进程。
O2O——shell为这个子进程调用execve函数,映射虚拟内存,进入程序入口后开始访问物理内存,然后调用main函数来执行目标程序。CPU为正在运行的hello发送逻辑控制流。当程序终止后,shell父进程回收shell进程,内核则删除与hello执行相关的数据内容。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位;VirtualBox/Vmware 15;Ubuntu 16.04 LTS 64位
开发与调试工具:code blocks,gcc,gdb,edb,readelf,hexedit等
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名 | 文件作用 |
hello.i | 预处理hello.c得到的文本文件 |
hello.s | 编译hello.i得到的汇编文件 |
hello.o | 汇编hello.s后得到的可重定位目标文件 |
hello | 链接hello后得到的可执行文件 |
hello.out | 反汇编hello后得到的可重定位文件 |
1.4 本章小结
本章简单阐释了hello的P2P和020环节,列出本文的实验环境、软件信息和中间结果,并大致介绍了hello.c在Linux中的生命周期过程。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理的概念——
CPP作为预处理器能够根据以“#”开头的命令行(例如头文件,宏定义等), 将源程序引用的所有库展开合并,从而将它修改称为一个文本文件。
预处理的作用——
1)将源文件中以”#include”格式包含的文件复制到编译的源文件中。
2)用实际值替换用”#define”宏定义的字符串。
3)根据”#if”后面的条件决定需要编译的程序代码。称”#if”为条件编译指令, 预处理能解读的这种指令包括#ifdef,#ifndef,#else,#esif,#endif等。
4)处理特殊符号,例如LINE解读为行号,FILE解读为当前处理的文件等。
2.2在Ubuntu下预处理的命令
命令:gcc hello.c -E -o hello.i
截图:
2.3 Hello的预处理结果解析
解析:经过预处理后hello.c转变为hello.i。打开hello.i文件,发现文件内容增加了很多,而且文字格式基本是C语言程序文本文件,这些内容主要是对源文件中的宏定义等进行了宏展开,例如头文件的内容被写进hello.i文件中(函数声明、结构体定义、定义变量、宏定义等内容)。此外,代码中的#define命令会对定义的字符串作相应的替换。
截图:
2.4 本章小结
本章介绍了预处理相关概念以及处理具体内容,例如替换宏定义符号、展开合并头文件内容、解读条件编译指令和特殊符号等。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念——
GCC作为编译器将文本文件hello.i翻译成文本文件hello.s,它的内容是一 个汇编语言程序。也就是说,编译就是一个将高级程序设计语言书写的源程序 转化为汇编语言或机器语言表示的目标程序的过程。
编译的作用——
将高级程序设计语言书写源程序转化为汇编语言或机器语言表示的目标程序。在这个过程中,GCC还能检查语法、调试措施、优化目标程序、合用不同语言等。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
截图:
3.3 Hello的编译结果解析
3.3.0程序的基本信息
.file:声明原文件。
.text:代码节。
.section:表示段内容。
.rodata:只读代码段。
.align:数据或指令的地址的对齐方式。这里是以8的倍数作为地址开始。
.string:声明一个字符串”Hello %s %s\n”。
.global:声明全局变量main。
.type:声明这个全局变量是数据还是函数。
3.3.1数据
1)字符串
在上面出现的.LC0和.LC1就是程序的两个字符串,它们位于只读数据段中。
在后面的汇编代码中,发现它们是作为printf函数的参数出现的。
2)局部变量i
在main函数中定义了一个局部变量i,在for语句中被初始化为0。所以在汇 编语言中应该是将局部变量i=$0放入堆栈中。
3)main的第一个参数:int argc
main函数的参数argc是由用户输入的,所以也应该放入堆栈中。
4)立即数
立即数在汇编代码中以”$x”的形式直接出现。
5)main的第二个参数:char *argv[]数组
这是本程序的唯一数组,数组的每个元素是一个指向char类型的指针。从汇 编代码可以看出,数组的起始地址存放在栈中-32(%rbp)的位置,并被两次调用, 都是将参数传送给printf函数。
3.3.2全局函数
main函数的函数体为:
int main(int argc, char *argv[])
前文的.global main说明main是一个全局函数。
3.3.3程序操作
1)赋值操作
程序的赋值操作主要是由mov指令实现的。这里赋值的是for语句中的i=0。
mov指令可以根据数据长度有不同的后缀,具体是:
b-一个字节 w-两个字节 l-四个字节 q-八个字节。
2)算术操作
本程序有i++的算术操作,所以有这样的汇编代码:
inc i
其实对于int,汇编代码有如下的算术运算操作:
指令 | 结果 |
leaq src,dst | dst=&src |
inc dst | dst++ |
dec dst | dst-- |
neg dist | dst=-dst |
add src,dist | dst+=src |
sub src,dst | dst-=drs |
还有mul,div,shift等算术操作 | 其他 |
3)数组操作
hello.c对于数组char *argv[]有调用操作atoi(argv[3])。
4)关系操作
第一个关系判断是argc!=4,其汇编指令如下。这句还有设置条件码的功能, 将会根据判断的结果将信息送入标志寄存器中,然后进行分支跳转。
第二个关系判断是i<8,其汇编指令如下。它是在for循环语句中出现的。
3.3.4程序控制转移
汇编语言先根据比较结果设置条件码,然后根据这些条件码进行控制转移。
hello.c涉及的控制转移指令为:
1)如果argc是4,不执行if语句;如果不是,则输出提示信息。
2)for(i=0;i<8;i++),每次判断i<8来决定循环是否继续(跳转到循环体Body)。
从上面的汇编代码可以看出,程序这部分的运行步骤为:先赋值i=0,然后无 条件跳转到循环体Body中,每次操作结束后将i自增1,然后判断i是否符合 条件(与i++相对应),若符合继续循环,否则跳出。
3.3.5函数操作
hello.c涉及的函数操作有:
main,printf,exit,sleep,getchar函数。其中main的参数是argc和argv;printf 输出两个字符串;exit退出函数运行,参数为1;sleep的参数是atoi(argv[3])。
而一般而言,如果函数P调用函数Q,会涉及如下操作:
1)传递控制——进行过程Q的时候,PC必须设置为Q代码的起始地址,这 样当从Q函数返回时,就可以从PC取出地址,然后PC设置为P中调用Q语 句后一条指令的地址。
2)传递数据——P必须能够向Q传递所需的若干参数,Q则必须能够向P进 行需要的操作或返回需要的一个值。
3)内存的分配与释放——在开始调用时,Q可能需要局部变量分配空间,而 在返回前又需要释放这些分配给Q的空间才行。
3.3.6类型转换
hello.c涉及的类型转换是atoi(argv[3]),也即ascii2int,就是将字符类型转换为 整数类型。编译还可以实现int,float,double,short,char之间的显隐式转换。
3.4 本章小结
本章详细描述了编译阶段中GCC作为编译器如何处理数据和操作,以及C语言中各种数据类型和函数操作,及其对应的汇编代码。通过了解编译器编译机制,我们能更好地处理将汇编代码翻译成C语言的问题。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念——
汇编器(as)将汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标 程序的格式,并将结果保存在.o目标文件中。.o是一种二进制文件,它包含程 序的机器指令编码。
汇编的作用——
生成可重定位文件.o,使得不同程序之间可以链接。同时解读汇编语言的助记 符,将汇编文件转化为机器指令。
4.2 在Ubuntu下汇编的命令
命令:gcc hello.s -c -o hello.o
截图:
4.3 可重定位目标elf格式
1)ELF Header
命令:readelf -h hello.o
解析:
从截图中可以看到,ELF头是以大小为16B的序列Magic开始的,它描述了声称该文件的系统字大小和字节顺序。操作系统在加载可执行文件时会确认Magic是否正确,如果不正确则拒绝加载: 第五个字节标识ELF文件是32位(01)还是64位(02)的;第六个字节标识该ELF文件字节序是小端(01)还是大端(02)的;第七个字节指示ELF文件的版本号,一般是01;后九个字节ELF标准未做定义,一般为00。
ELF头的其余部分包含帮助链接器语法分析解释目标文件的信息,它包括ELF头大小、目标文件类型、机器类型、字节头部表的偏移,以及节头部表的条目的数量及各自的大小等信息。
从截图的内容可以发现文件是可重定位目标文件,有13个节。
截图:
2)Section Header
命令:readelf -S hello.o
解析:
节头部表包含了文件中出现的各个节的意义,指明了节的类型、位置和大小等信息。由于是可重定位目标文件,所以每个节都是以逻辑地址0开始的,这样可以重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各个节在文件中的起始位置及其所占空间大小。节头部表还指明了各个节的操作权限,例如代码段可执行但不可写;只读数据段不可写也不可执行等。
截图:
3).symtab
命令:readelf -s hello.o
解析:
符号表存放程序中定义和引用的函数以及全局变量的信息。name是符号名称;value对于可重定位目标模块而言是符号相对于目标节的起始位置偏移,对于可执行目标文件则是该值的绝对运行地址;size是目标大小;type不是数据就是函数;bind字段则指明符号是本地的还是全局的。
截图:
4).rela.text
命令:readelf -r hello.o
解析:
重定位节包括.text节中需要重定位的信息,当连接器把这个目标文件和其他文件组合时需要修改这些位置。重定位节中各项符号的信息是:Offset是被修改引用节的偏移Info,包括symbol(标识被修改引用应该指向的符号)和type(重定位类型),依次占4字节;Type指明链接器应如何修改新应用;Attend是一个有符号常数,一些重定位要使用它对被修改引用的值调整偏移;Name则是重定向到的目标名称。
截图:
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o
截图:
hello.s的内容截图如下:
解析:
通过将反汇编代码与hello.s比较,发现它与汇编语言的指令没有多大差别,只是反汇编代码所表示的除了汇编指令外还包含指令对应的机器码。
机器码是二进制机器指令的集合,是计算机唯一可以识别并执行的语言。按照小端法解读机器码,发现机器指令由操作码和操作数组成。
汇编语言是机器码的自然化,它利用人们相对熟悉的词句作为助记符直接指明CPU对该指令所采取的操作和反应。汇编语言的操作码与机器码成一一对应的映射关系,所以汇编语言可以转化为机器语言。
当然,二者之间的区别还是不容忽视的,例如:
1)函数调用:
在.s文件中,函数调用语句后面直接跟着函数名称,而反汇编的调用语句的目标地址是调用者下一条指令的地址。这种不同是因为hello.c中调用的函数都是共享库函数,最终需要借助动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言时,对于这些不确定地址的函数调用,必须将其call指令之后的相对地址设置为0,然后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步处理。
2)分支跳转:
反汇编不会有.L3这种跳转目的地标号,跳转的目的地应当是一个确定的地址。这是因为标号也属于一种助记符,这在反汇编中是不可能存在的。
4.5 本章小结
本章对hello.s进行了汇编生成.o可重定位文件,并解析了它所蕴含的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-linu x-gnu/crt1.o/usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /u sr/lib/x86_64-linux-gnu/crtn.o
截图:
5.3 可执行目标文件hello的格式
1)ELF头:
与hello.o文件的ELF头不同的是,Type类型变为EXEC(可执行)。而且段 的数量增加了。
2)节头部表:
对hello中所有节信息进行了声明,其中包括大小和程序中的对应偏移量,因 此根据节头部表的信息我们就可以用HexEdit定位各个节所占区间。而地址就 是程序被载入到虚拟内存的起始地址。
3)重定位节:
4)符号表:
5.4 hello的虚拟地址空间
使用edb加载hello,查看进程的虚拟地址空间各段信息,并与5.3对照分析说明。通过查看edb,找到hello的虚拟地址空间开始于0x400000,结束于0x400ff0。
截图:开始
结束
接着,可以根据5.3中节头部表读出各个节的信息。例如:
1).init段
2).text段
3).rodata段
5.5 链接的重定位过程分析
命令:objdump -d -r hello>hello.out
截图:
hello.out内容截图:
分析:
1)hello与hello.o的不同:
从上面的截图可以发现一些不同:一方面,hello反汇编代码有确定的虚拟地址,即重定位成果,而hello.o反汇编代码中虚拟地址均默认为0,说明没有重定位;另一方面hello反汇编多了很多节和很多函数的汇编代码,说明功能有所增加。
具体而言,hello 的ELF头中部分多出的节的功能如下:
节名称 | 功能 |
.init | 程序初始化执行的代码 |
.plt | 动态链接过程链接表 |
.dynamic | 存放被ld.so使用的动态链接信息 |
.data | 经过初始化的数据 |
.comment | 一串由编译器生成的字符串 |
.interp | 保存ld.so的路径 |
.note.ABI-tag | Linux独有的节 |
.hash | 符号的哈希表 |
.gnu.hash | GNU拓展符号的哈希表 |
.dynsym | 存档.dynsym节中的符号名称 |
.rela.dyn | 运行时重定位表 |
.rela.plt | .plt节重定位条目 |
.init | 程序初始化需要执行的代码 |
.fini | 当程序正常终止时需要执行的代码 |
2)hello重定位过程:
①重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
②重定位节中的符号引用时,链接器修改代码节和数据节中对每个符号的引用,使他们指向实际运行时的地址。执行这一步,链接器依赖于可重定位目标模块中的重定位条目。
③重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。代码的重定位条目放在.rel.text中。在这个段中R_X86_64_32表示32位绝对寻址,R_X86_64_PC32表示相对寻址,其余量在前面已经介绍过了。基于这些认识,我们看重定位地址计算算法:
foreach section s
{
foreach relocation entry r
{
refptr = s + r.offset;
if (r.type == R_386_PC32) {
refaddr = ADDR(s) + r.offset;
*refptr = (unsigned) (ADDR(r.symbol) + *refptr - refaddr);
}
if (r.type == R_386_32) {
*refptr = (unsigned) (ADDR(r.symbol) + *refptr);
}
}
}
这样我们可以验证call指令的跳转值是多少。例如对于sleep()函数,假设它相对地址引用定义sleep()的重定位条目为r,则addr(s)=0x400540,refaddr= addr(s)+r.offset=0x4005f5+0x27=0x40061C,*refptr=(0x400540-4-0x40061C)=(unsig ned)0xffffff46,与反汇编结果相符。
5.6 hello的执行流程
如果按照程序运行的几个重要阶段(开始,初始化,main函数调用,程序结束等)划分的话,执行流程为:
程序名称 | 程序地址 |
ld-2.27.so!_dl_start | 0x00007f5712654ea0 |
ld-2.27.so!_dl_init | 0x00007f57126637d0 |
hello!_start | 0x400500 |
libc-2.27.so!_libc_start_main | 0x00007f5711867ab0 |
libc-2.27.so!_cxa_atexit | 0x00007f5711889430 |
libc-2.27.so!_libc_csu_init | 0x4005c0 |
libc-2.27.so!_setjmp | 0x00007f5711885350 |
libc-2.27.so!exit | 0x00007f571188a008 |
截图示例:
5.7 Hello的动态链接分析
动态链接的基本思想:
首先是链接时符号重定位,是指在链接阶段对符号进行重定位,这时链接器在把目标文件合并成一个可执行文件并分配好各段的加载地址后,就可以重新计算那些需要重定位的符号的具体地址了。接着是加载时符号重定位,其原理与链接时符号重定位一致,只不过把重定位的时机放到了动态库被加载到内存之后,由动态链接器来进行。尽管如此,动态链接与静态链接有着两个本质的不同:一方面,链接器在构建可执行文件的时候,会在当前可执行文件的数据段里分配出相应的空间来作为该符号真正的内存地址,等到运行时加载动态库后,再在动态库中对该符号的引用进行重定位:把对该符号的引用指向可执行文件数据段里相应的区域;另一方面ELF 文件对调用动态库中的函数采用了所谓的延迟加载策略, 只有当该函数在其第一次被调用发生时才最终被确认其真正的地址,因此我们不需要在调用动态库函数的地方直接填上假的地址,而是使用了一些跳转地址作为替换,这样一来连修改动态库和可执行程序中的相应代码都不需要进行了。然后是处理地址无关代码,进行函数调用时要将返回地址压到栈上,此时通过读这个栈上的值就可以获得下一条指令的地址了。最后还需要延迟加载,简单地说就是应该等到第一次发生对该函数的调用时才进行符号绑定。
动态链接的过程:
上述延迟绑定是通过GOT和PLT实现。GOT是数据段的一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目;而PLT是代码段的一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
拿hello进行分析,具体如下:
首先找到GOT的起始地址为0x601000,然后在EDB中查看其值,发现在dl_init之前0x601008后的16个字节都是0。
调用dl_start后发生改变,0x601008后16个字节的内容变为0x7fc324781 170,0x7fc32456d8f0。其中0x600e26为GOT[0],以及GOT[1]包含动态链接器在解析函数时会用到的信息;GOT[2]则是动态链接器在ld_linux.so模块的入口。其余,每个条目对应一个被调用的函数。
例如,考虑puts@plt函数:
在执行这个函数之前,0x601008后16字节应当全部为0。
执行经过PLT函数后,观察Data Dump内容如下:
这个变化说明已经发生了动态链接。
5.8 本章小结
本章详细说明了链接的机制和作用,并展示了可重定位文件hello.o到可执行文件hello的转变过程。利用READELF和GDB工具展示了hello.o和hello的ELF格式和各个节的内容。到此我们查看了hello的虚拟地址计算、重定位过程、动态链接过程,最终hello.c可以被执行了。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念——
进程是一个实体,是一个执行中的程序实例,每一个进程都有它自己的实际地址空间。一般情况下,这个地址空间被划分为文本区、数据区和堆栈区域。文本区存储这个程序的执行代码;数据区存储变量和执行进程时的动态内存分配;堆栈区域则保存活动过程中调用的指令和局部变量。
进程的作用——
一方面,进程有利于程序管理,其中进程控制是进程管理中最基本的功能,它用于创建一个新进程,终止一个已完成的进程,或者去终止一个因出现某事件而使其无法运行下去的进程,还可负责进程运行中的状态转换,使得计算机能够对程序做出合理反映。
另一方面,进程有利于处理器在不同程序间切换,这就造成了一个假象,就是我们的程序好像是系统中当前运行的唯一程序一样。处理器好像是无间断的执行我们程序中的指令。
6.2 简述壳Shell-bash的作用与处理流程
Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。
其基本功能是解释并运行用户的指令,重复如下处理过程:
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
(3)检查第一个(或首个)命令行参数是否是一个内置的shell命令
(4)如果不是内部命令,调用fork( )创建新进程/子进程
(5)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(6)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait)等待作业终止后返回。
(7)如果用户要求后台运行(如果命令末尾有&号),则shell返回。
这样看来,shell就成功实现了代表用户管理进程的。
6.3 Hello的fork进程创建过程
用户在终端输入对应的指令./hello,shell就会读取输入的命令,并开始进行以下操作:首先判断输入指令是否是内置的,发现hello不是一个内置的shell指令,所以调用应用程序,找到当前所在目录下的可执行文件hello,准备执行;接着Shell会自动的调用fork()函数为父进程创建一个新的子进程,子进程就会因此得到与父进程(shell)的虚拟地址空间相同的一段各种的数据结构的副本(包括代码和数据段,堆,共享库和用户栈)。父进程与子进程最重要的不同在于他们PID不同。其实父进程与子进程分别是两个并发的进程,在子进程中程序运行的这个过程中,父进程在原位置等待着程序的运行完毕,这里可以看出二者在时间上是并发的。
6.4 Hello的execve过程
当创建了一个子进程之后,子进程调用exceve函数加载并运行可执行目标文件hello,且包含相对应的一个参数列表argv[]和环境变量的列表exvp[]。只有当函数出现错误时,例如找不到hello文件时,execve才会返回-1,否则execve调用成功,它不会产生返回。这个加载并运行需要以下几个步骤:
1)Loader子进程现有的虚拟内存段,创建一组新的段(栈与堆初始化为0),并将虚拟地址空间中的页映射到可执行文件的页大小的片。这个虚拟内存的映射关系如下图所示。
2)映射共享区域。若hello程序与共享对象链接,例如标准C库libc.so,那么它们之间都是动态链接到的,之后再映射到用户虚拟地址空间中的共享区域。
3)函数设置程序计数器PC。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
6.5 Hello的进程执行
进程提供给应用程序以下两种抽象:
1) 一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器,通过OS内核的上下文切换机制提供。
2) 一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用CPU内存,OS内核的虚拟内存机制提供。
hello进程的执行是依赖于进程所提供的抽象的基础上,下面阐述操作系统所提供的的进程抽象:
①逻辑控制流::一系列程序计数器 PC 的值的序列叫做逻辑控制流,进程是轮流 使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被暂时挂起,然后轮到其他进程。
②并发:每个进程是个逻辑控制流,如果两个逻辑流在时间上有重叠,则称这两个进程是并发的(并发进程),否则他们是顺序的。
③时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
④私有地址空间:进程为每个流都提供一种假象,好像它是独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个内存字节是不能被其他进程读或者写的,在这个意义上,这个地址空间是私有的。
⑤上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。进程的上下文分为系统级上下文(系统标识、现场、控制信息)和用户级上下文(用户堆栈、数据、程序和共享地址空间)。
⑥上下文切换:当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。
⑦用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
下面利用hello对上面的过程进行分析。
命令:./hello 1190300505 张晟哲 1
分析:
在调用进程调用sleep之前,hello在当前的用户内核模式下继续运行,在内核中进程再次调用当前的sleep之后进程转入用户内核等待休眠模式,内核中所有正在处理休眠请求的应用程序主动请求释放当前正在发送处理sleep休眠请求的进程,将当前调用hello的进程自动加入正在执行等待的队列,移除或退出正在内核中执行的进程等待队列。之后设置程序计时器,休眠自己设置的时间,当计时器时间归0,发送一个中断信号。内核收到中断信号进行中断处理,hello被重新加入运行队列,等待执行。
截图:
6.6 hello的异常与信号处理
1)异常类型:
类别 | 类型 | 产生原因 | 返回情况 |
中断 | 异步异常 | 设备I/O信号 | 返回到下一条指令 |
陷阱 | 同步异常 | 有目的的异常 | 返回到下一条指令 |
故障 | 同步异常 | 可能可恢复的错误 | 可能返回目前指令 |
终止 | 同步异常 | 不可恢复的错误 | 不返回 |
2)异常信号类型:
异常信号 | ID | 默认行为 | 相应事件 |
SIGINT | 2 | 终止 | 来自键盘的中断 |
SIGKILL | 9 | 终止 | kill命令 |
SIGSEGV | 11 | 终止 | 段错误 |
SIGALRM | 14 | 终止 | alarm定时器信号 |
SIGCHLD | 17 | 忽略 | 一个子进程停止或终止 |
3)键盘操作引发的各类异常:
首先正常运行一次程序:
然后按下Ctrl-C:
这种操作会导致内核发送一个SIGINT信号到前台进程组的每个进程。用ps查看前台进程组中是否还有hello进程,发现没有:
然后按下Ctrl-Z:
这种操作会导致前台作业被挂起,hello并没有被回收,而是于后台运行。用ps查看进程组中是否还有hello进程,发现有:
使用fg 1命令继续执行hello进程,在输入字符串后正常退出:
再一种操作就是乱按键盘,可以发现在按入enter键之前字符是一直输入的,这是因为键盘输入只是被缓存到stdin文件中,只有输入\n时getchar才认为这时完成了一次字符串输入。
6.7本章小结
在本章中,我们阐释了进程的含义和作用,同时介绍了shell的一般处理流程和作用。我们还借助hello理解了shell如何用fork创建子进程,execve执行进程,以及系统的异常类型与信号处理方法。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址——
逻辑地址程序经过编译后出现在汇编代码中的地址,它是用来指定一个操作数或者是一条指令的地址。逻辑地址是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。
线性地址(虚拟地址)——
线性地址和逻辑地址类似,也不是一个真实的地址。具体的格式可以表示为[虚拟地址描述符:偏移量]。在分页地址变换机制中需要使用一个线性分页地址描述符作为输入,而线性地址可以再经过物理地址变换以产生一个新的物理分页地址。
物理地址——
物理地址用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。可以直接把物理地址理解成插在机器上那块内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址。指令最终通过物理地址才能成功的取到需要的操作数。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,表示具体的是代码段寄存器还是栈段寄存器抑或是数据段寄存器。这里的索引号就是段描述符的索引,它具体指向一个段。很多这样的段聚在一起就构成了段描述符表。而段描述符表又分为全局描述符表GDT和局部段描述符表LDT。根据它们我们就能对逻辑地址和线性地址做出转换了。
而段式管理的分段功能在实地址模式和保护模式下有所不同。实地址模式中逻辑地址=线性地址=实际的物理地址。段寄存器存放真实段基址,同时给出32位地址偏移量,则可以访问真实物理内存;保护模式中线性地址还需要经过分页机制才能够得到物理地址,线性地址也需要逻辑地址通过段机制来得到。段寄存器用于存放段选择符,通过段选择符可以得到对应段的首地址。处理器在通过段式管理寻址时,首先通过段描述符得到段基址与偏移量结合得到虚拟地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
特别注意虚拟内存中的虚拟页,在任何时候它都只可能属于三者之一:
未分配的:VM系统还未分配的页。
缓存的:当前已经缓存在物理内存的已分配页。
未缓存的:当前未缓存在物理内存的已分配页。
每次将虚拟地址转换为物理地址,都会查询页表来判断一个虚拟页是否缓存在DRAM的某个地方,如果不在DRAM的某个地方,通过查询页表条目可以知道虚拟页在磁盘的位置。页表将虚拟页映射到物理页。如下图所示:
基于上面的认识,一个虚拟地址包含虚拟页面偏移(VPO),虚拟页号(VPN)两部分,MMU利用VPN选择适当的PTE,根据它我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。VPO和PPO相等。如果虚拟页是未缓存的,会触发缺页故障。调用缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行导致缺页的指令。
7.4 TLB与四级页表支持下的VA到PA的变换
若采用快表策略,TLB是MMU中一个小的具有高相联度的集合,它能够实现虚拟页码向物理页码的映射,而对于页码数很少的页表可以完全包含在TLB中当我们使用MMU向TLB的一个组请求一个页表中的条目时,L1通过VPO位在页表中查找一个相应的数据组,并在页表中读出这个组里的个数据标记和相应的数据关键字。当MMU从TLB的这个组得到PPN时,代表缓存的工作在这个组的请求之前已经完全准备好,可以与数据标记文件中的一个虚拟地址进行匹配了。
而如果采用多级页表策略,例如四级页表层次结构,每个四级页表进程都有他自己私有的页表层次结构,这种设计方法从两个基本方面就是减少了对内存的需求,如果一级页表的PTE全部为空,那么二级页表就不会继续存在,从而为进程节省了大量的内存,而且也只有一级页表才会有需要总是在一个内存中。四级页表的层次结构操作流程如下:36位虚拟地址被寄存器划分组成四个9位的片,每个片被寄存器用作一个页表中偏移量。CR3寄存器内储存了一个L1页表的一个物理起始基地址,指向第一级页表的一个起始和最终位置,这个地址是页表上下文的一部分信息。VPN1提供了到一个L1PTE的偏移量,这个PTE寄存器包含一个L2页表的起始基地址。VPN2则提供了到一个L2PTE的偏移量,一共四级,逐级以此层次类推。
7.5 三级Cache支持下的物理内存访问
下面我们只论述L1的物理内存访问规律,因为另外两级原理相同。
一级Cache的物理访存过程为:
1)组选择:根据取出的虚拟地址的组索引找到相应的缓存组。
2)行匹配:用标志位跟找到的缓存组比较,若标志位相同且高速缓存行的有效位为1则本次缓存命中,否则不命中。
3)字选择:命中后我们根据块内偏移量找到所需字节,将其上内容交给CPU。
4)不命中处理:如果本次高速缓存不命中,则需要从存储层次结构的下一层取出所需的块,然后将这个新块在原来找到的位置保存在一个行中。如果这个组有空闲则直接放入,如果没有则只能驱逐最近最少使用的行再放入。
7.6 hello进程fork时的内存映射
Shell通过一个调用fork函数可以让进程内核自动创建一个子进程,这个新的进程拥有新的数据结构,并且被内核分配了一个独有的pid。它有着自己独立的虚拟内存空间,自己独立的逻辑控制流,以及当前已经可以打开的各类文件信息和页表的原始数据和样本。为了保护进程的私有数据信息并节省内存消耗,进程的每个数据区域都被内核标记起来标明只能写入时复制。
7.7 hello进程execve时的内存映射
Hello进程execve时的内存映射主要分为以下方面:
1)删除已存在的用户区域,可能会自动覆盖当前进程的所有虚拟地址和空间,删除当前进程虚拟地址的所有用户已存在的代码共享区域和数据结构等等。
2)映射私有区域,为新程序的代码、数据、.bss和栈区域创建新的区域结构。
3)映射共享区,让hello与系统执行文件链接映射到共享区域。它首先映射到一个共享的区域,hello这个程序与当前共享的对象libc.so链接,它可能是首先动态通过链接映射到这个代码共享程序上下文中的。然后再通过映射链接到用户虚拟地址和部分空间区域中的另一个共享代码区域内。
4)设置程序计数器PC,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障是指用户想要写内存地址(虚拟地址)时该地址对应的物理页不在内存,而是在磁盘中的一种可恢复的故障。这时,系统调用缺页处理程序,选择牺牲页面。具体步骤为:
1)CPU为MMU生成并传送一个虚拟地址。
2)地址管理单元生成PTE,高速缓存或主存向MMU传送PTE。
3)PTE的有效位0触发一次MMU异常,传递CPU的控制信号到操作系统内核中执行缺页异常处理程序。
4)处理程序决定物理内存中的牺牲页,如果这个页是非空的就把它换到磁盘。
5)处理程序调入新页面并更新主存中的PTE。
6)处理程序返回,程序继续执行触发缺页异常的那条指令。
7.9动态存储分配管理
1)动态内存分配器的基本原理:
动态内存分配器维护者一个进程的虚拟内存空间,叫做堆(heap)。堆紧接着未初始化的数据区域开始,向更高的地址生长(所谓向上生长)。对于每个进程,内核时钟维护者一个指向堆顶部的指针变量brk。
分配器将堆视为一组大小不同的块的集合进行维护。每个块是一个连续的虚拟内存片区,要么是已分配的,要么就是空闲的。已分配的块显式地保留被程序使用,而空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为显式和隐式两种。前者要求程序显式地释放所有已分配的块;后者要求分配器检测每个分配块何时不再使用,然后到时释放它,实现垃圾收集。
2) 带边界标签的隐式空闲链表分配器原理:
隐式空闲链表区别块的边界、已分配块和空闲块的方法如图所示:
这样看来,一个块是由一个字的头部、有效荷载和可能的填充组成的。头部的编码指明这个块的大小(包括头部和填充),并在最后一位指示这个快的已分配的还是空闲的。头部后面是malloc函数调用时请求的有效荷载,而在后面是暂时不是欧阳的填充块。所以隐式空闲链表就是将空闲块通过头部块的大小字段隐含连接的结构,它的示意图如下:
具体的步骤为:首先当一个程序请求一个k字节块时,分配器搜索空闲链表。搜索策略有首次适配、下一次适配和最佳适配等,目的是找到一个足够大的可以满足要求的空闲块。一旦找到,必须决定这个块分配多少空间。分配器通常将空闲块已分割为两部分,前者拿去分配,后者保持空闲。如果找不到,就考虑合并物理内存上相邻的空闲块,如果仍然不够就只能由分配器调用sbrk函数,向内核申请额外的内存。
3) 显式空间链表的基本原理:
显式空闲链表是将空闲块组织为某种形式的显式数据结构。堆被组织为一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继的指针。使用双向链表而不是隐式空闲链表使得首次适配的时间从块数的线性时间减小到空闲块数的线性时间。
一种方法是,使用后进先出的顺序维护链表,将新释放的块在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成;还可以按照地址顺序来维护链表,其中链 表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要 线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比 LIFO 排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。
7.10本章小结
本章介绍了hello在存储器中的存储空间,介绍了四种地址空间的差别和互换。我们还针对hello介绍了段式管理、页式管理的基本原理,阐述了三级Cache的物理内存访问原理,Shell中fork,execve函数的内存映射,缺页故障处理以及动态存储分配管理等重要概念。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。
设备的模型化:文件。
设备管理:Unix I/O接口。
8.2 简述Unix IO接口及其函数
Unix I/O 接口:
1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想 要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在 后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文 件的所有信息。
2)Shell创建的每个进程都有三个打开文件:标准输入,标准输出,标准错误。
3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行 seek,显式地将改变当前文件位置k。
4)读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文 件位置 k 开始,然后将 k 增加到k+n,给定一个大小为m字节的而文件,当k>=m 时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5)关闭文件:内核释放文件打开时创建的数据结构,并将这个描述符恢 复到可用的描述符池中去。
Unix I/O 函数:
1)int open(char* filename,int flags,mode_t mode),进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在 进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访 问这个文件,mode参数指定了新文件的访问权限位。
2)int close(fd),fd是需要关闭的文件的描述符,close 返回操作结果。
3) ssize_t read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
4)ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
首先查看printf函数的函数体:
static int printf(const char *fmt, ...)
{
va_list args;
int i;
va_start(args, fmt);
write(1,printbuf,i=vsprintf(printbuf, fmt, args));
va_end(args);
return i;
}
printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。
write函数体则如下:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
在printf中调用系统函数write(buf,i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,
int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。
再来看看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将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。于是我们的打印字符串就显示在了屏幕上。
从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码,直到接受到回车键才返回这个字符串。Getchar的大概思想是读取字符串的第一个字符之后再进行返回操作。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及函数,分析了printf和getchar函数。这些能够帮助我们更好地理解系统级I/O的工作模式。
(第8章1分)
结论
在整篇文章中我们详细研究了hello.c的生命历程,见证了:
hello.c——
hello.c的诞生,是从高级语言C语言开始的。
hello.i——
hello.c经过预处理变成hello.i,这时它内容得到了极大的丰富,特别是借助计算机前辈们写出的#include头文件来壮大了自己。
hello.s——
hello.i经过编译脱胎换骨,变成hello.s时以全新的姿态——汇编语言展现在我们面前。它变得没有高级语言那么好懂了,但至少有助记符让我们看懂了它。
hello.o——
hello.o是hello.s汇编的产物,这时我们已经不能直接阅读、了解它了。
hello——
hello.o经过链接后进一步茁壮成长,最终迎来了它生命的高潮——可执行文件hello。这时的它已经是难以阅读的机器编码了,但是它以程序执行、输入输出的新形势与我们展开了对话。
fork创建子进程——
输入非内置指令会引起shell调用fork()创建一个子进程。
execve加载——
shell调用execve,execve 调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main 函数。
上下文切换——
hello调用sleep函数之后进程陷入内核模式,处理休眠请求主动释放当前进程,内核进行上下文切换将当前进程的控制权交给其他进程,当sleep函数调用完成时,内核执行上下文切换将控制传递给当前进程。
动态内存分配——
当hello调用print函数时,会使用malloc向动态内存分配器申请堆中的内存。
信号控制——
系统会根据我们发出的Ctrl-C和Ctrl-Z发送信号SIGINT和SIGTSTP给hello进程,影响它的执行进度,是我们与他最直接的对话。
终止——
当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有 数据结构。这时hello最终结束了自己的生命周期,走向了沉寂。
学习完了计算机系统这门课,我的感触颇深,收益匪浅,具体如下:
1)再次证明计算机是一门基于抽象的学科。从计算机系统这门课来看,二进制机器码是各种信息的抽象(例如任何程序员编写出的高级语言最终到机器上都是转变为机器码进行运行);进程是对程序执行与系统级存储和系统级I/O的抽象(整个进程实际是需要CPU为并发进程统筹这些内容的,但进程给我们相应的假象);虚拟内存是对物理存储的抽象;文件是对系统级I/O的抽象,等等。
2)一个体系的形成,需要许许多多精密的设计与考量。计算机现在的普及绝非一朝一夕,其中靠着多少先人的心血才能够将计算机设计的如此方便、快捷。例如为了增加寻址范围就改变指令的寻址方式;为了解决存储设备的存储量与存取速度之间的不平衡,设立了高速缓存的机制;为了防止计算机被人攻击设计了栈随机化和栈金丝雀等保护措施,等等等等。另外,还必须考虑在多种条件下计算机的处理方式,例如对于程序的中断、陷阱、故障的不同处理措施。所以,将来创新创业时不仅仅要考虑全面,而且不能对成功妄图一蹴而就.
(结论0分,缺失 -1分,根据内容酌情加分)
附件
文件名 | 作用 |
hello.i | hello.c预处理产物 |
hello.s | hello.i编译产物,展示与hello.c功能相同的汇编语言 |
hello.o | hello.s汇编产物,是一种可重定位目标文件 |
hello | hello.o链接产物,是一个可执行程序 |
hello.out | hello反汇编产物,能够帮助分析hello文件的机理 |
hello.o.txt | hello.o反汇编产物,能够帮助分析hello.o的机理 |
readelf1.txt | hello.o的ELF信息 |
readelf2.txt | hello的ELF信息 |
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.
(参考文献0分,缺失 -1分)