CSAPP大作业

计算机系统

大作业 注:含图片版可在我的主页-资源内免费下载

题 目 程序人生-Hello’s P2P
专 业 计算机类
学   号 1190201709
班   级 1903011
学 生 杨忠博  
指 导 教 师 史先俊

计算机科学与技术学院
2021年5月
摘 要
一个程序,从编译到运行再到回收,只有短短的几分钟。然而在这短暂的一瞬里计算机系统的内部有很多软硬件协调运作,才让程序完成了它的工作。本文章将以最简单的hello.c程序为第一视角,探索其在linux系统下的整个生命周期,运用各种工具从底层细致地还原hello.c的“一生”,从而加深对计算机系统的理解。
关键词:hello.c;linux系统;生命周期;计算机系统;程序的一生。

(摘要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过程:
Program:程序员输入程序hello.c
Process:GCC编译器驱动程序读取hello.c,Process分为四个阶段:1)hello.c经过预处理器(cpp)处理后得到另一个C程序hello.i .2)修改了的源程序hello.i经过编译器(ccl)编译后得到汇编程序hello.s .3)hello.s经过汇编器(as)被翻译为机器语言hello.o .4)hello.o经过链接器链接,得到可执行文件。可执行文件执行即可输出语句。

O2O过程:

1.shell为hello进程execve,映射虚拟内存,进入程序入口后程序开始载入物理内存。
2.进入 main 函数执行目标代码,CPU为运行的hello分配时间片执行逻辑 控制流。
3.当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。

1.2 环境与工具
硬件环境:Intel Core i7-9750H x64CPU,16G RAM,256G SSD, Nvidia GTX 1660ti.
软件环境:Ubuntu16.04.1 LTS
开发与调试工具:vim,gcc,as,ld,edb,readelf,HexEdit
1.3 中间结果
中间结果文件:
1.hello.i 预处理之后的文本文件
2.hello.s 汇编后的汇编文件
3.hello.o 汇编后的可重定位执行文件
4.hello 链接后的可执行文件
5.hello.elf hello的elf格式
6.helloo.elf hello.o的elf格式
7.hello.objdump hello的反汇编
8.helloo.objdump hello.o的反汇编

1.4 本章小结
本章主要介绍了hello.c的P2P和O2O过程,同时梳理了P2P,O2O的理论,介绍了实验的软硬件环境,给出了P2P和O2O中间结果的生成文件。

(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
概念:
预处理器(cpp)中会展开以#起始的行,试图解释为预处理指令(preprocessing directive) ,其中 ISO C/C++要求支持的包括#if、 #ifdef、 #ifndef、 #else、 #elif、 #endif(条件编译)、 #define(宏定义)、 #include(源文件包含)、 #line(行控制)、 #error(错误指令)、 #pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。
作用:
主要处理源代码中的以“#”开始的预编译指令,如:“#include”、“#define”等,其主要处理规则如下:
1)将所有的“#define”删除,并且展开所有的宏定义。
2)处理所有条件预编译指令,如:“#if”、“#ifdef”、“#else”、“#endif”。
3)处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。该过程是递归进行的,因为被包含的文件可能还包含其他文件。
4)删除所有的注释“//”和“/ /”。
5)添加行号和文件名标识,比如#2 “hello.c” 2,以便于编译时编译器产生调试试用的行号信息以及用于编译时产生编译错误或警告时能够显示行号。
6)保留所有的#pragma编译器指令,因为编译器须要试用他们。
7)预编译生成的.i文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到.i文件中。所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。

2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析

点开hello.i文件,可以发现其中包含共3060行代码。

最后的几行(从3046行开始)为hello.c的代码内容

hello源文件中有三条预处理命令:#include <stdio.h>、#include <unistd.h>、#include <stdlib.h>,预处理程序将三个文件中的内容拷贝到预处理文件中,即为以上1~3045行的内容,3146行到最后则是直接复制源文件的内容。如果遇到头文件中包含预处理命令,则递归地将内容复制进去,直到预处理文件中不包含任何预处理命令为止。最终执行结果为替换掉定义的宏,代码中的注释也会删除,将#include的内容都插入进来。

2.4 本章小结
预编译过程是程序在编译系统中执行的第一个过程,在这个过程中预处理器(cpp)会根据源程序中以字符#开头的命令来修改源程序,在hello.i文件中可以看到将 stdio.h 文件包含进来。这个过程的结果是将一些用到的、涉及到的函数库头文件复制到程序源文件中、在程序代码中替换宏定义,并根据条件编译的条件保留相应的内容。
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
概念:
就是把预处理生成的文件进行一系列词法分析、语法分析、语义分析、优化,生成相应的汇编代码文件。这个过程是整个程序构建的核心部分,也是最复杂的部分之一。
作用:
编译除了基本的翻译操作,还包含语法检查和目标程序优化等功能。在翻译地过程中动态地检查语法错误,并将错误实时地反映出错误的类型和部位等有关信息。目标程序优化分为表达式优化、循环优化或程序全局优化,用以提高目标程序的质量,即占用的存储空间少,程序的运行时间短。

注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析
3.3.1数据和赋值
常量:字符串常量,分别对应LC0和LC1标签。调用时采用了相对寻址,用$rip+标签LC0或LC1的值即可找到相应常量。

数字常量,L2,L3,L4,LF86中均有数字常量,以立即数的形式进行着运算。

变量:整型变量i,属于局部变量存在堆栈中,用于for循环。

i在汇编代码中表示为$rbp-4

表达式:共有四个表达式。
1)argc!=4,在.s文件中编译器将其定义为

相等则跳转到L2接着跳转到L3进行循环,不相等则结束。
2) for(i=0;i<8;i++),其中i=0,是对变量i的赋值操作,在.s文件中编译器将其定义为

将立即数0赋给变量i
3) for(i=0;i<8;i++),其中i<8是个条件判断式,如满足i<8则继续循环,否则跳出循环,在.s文件中编译器将其定义为

判断i是否小于等于7,因为i是整型≤7和<8的含义是一样的,若满足则跳转到L4
4) for(i=0;i<8;i++),其中i++,在每次循环结束执行,表示为i=i+1,在.s文件中编译器将其定义如下

类型:此程序仅包含字符串类型和有符号数类型。
3.3.2 类型转换
程序使用了atoi函数,将字符串类型转换为了有符号数类型。

3.3.3 算数操作与关系操作
算数操作:共包含4次加操作,和一次减操作,加操作为累加器%rax分别加自然数16,8,24和局部变量i加自然数1。减操作为栈指针%rsp减自然数32
加操作%rax+16
加操作%rax+8
加操作%rax+24
加操作-4(%rbp)+1
减操作%rsp - 32
关系操作:共有2次关系操作,分别为判断argc与立即数4的相等关系,判断局部变量i和立即数7是否满足小于等于关系。

i==4

i<=7
3.3.4 数组/指针/结构操作
对于原函数中的参数*argv[],在.s文件中先将-32(%rbp)赋给累加器%rax,然后%rax自加16,取其值赋给%rdx,%rdx中存的即为argv[1]参数。

同理将-32(%rbp)赋给累加器%rax,然后%rax自加8,取其值赋给%rsi,%rsi中存的即为argv[2]参数。

将-32(%rbp)赋给累加器%rax,然后%rax自加24,取其值赋给%rdi,再将%rdi调用atoi函数可知,%rdi存的即为参数argv[3]。

3.3.5 控制转移
将参数i初始化为0后直接跳转到L3

比较局部变量i和立即数7的大小,若i≤7则跳转到L4

比较-20(%rbp)内参数argc与立即数4的大小,若argc等于4则跳转到L2

3.3.6 函数操作
函数调用:
共有6次函数调用,调用方式为call直接加函数名加@PLT,分别调用了puts函数、exit函数、printf函数、atoi函数、sleep函数、getchar函数。

参数传递和函数返回:
main函数中传递了两个参数argc和*argv[],分别用%edi和%rsi中存储

main返回累加器%rax,返回时先将其值赋为0.

puts函数call puts时只传入了字符串参数首地址;

Printf函数call printf时传入了 argv[1]和argc[2]的地址。

exit函数:传入的参数为1,再执行退出命令

sleep函数:传入参数atoi(argv[3]),

getchar函数:无传递参数,在main中被调用

3.4 本章小结
本章首先介绍了编译的概念、过程及作用。接着介绍了将hello.i编译为hello.s的命令行语句。最后结合C代码和汇编代码,两向对比,从数据和赋值、类型转换、算术操作与关系操作、数组指针结构操作、控制转移、函数操作等方面介绍了程序是如何从高级语言程序转换为底层的汇编代码,并从机器的角度描述程序的形为。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
概念
驱动程序运行汇编器as,将汇编语言(这里是hello.s)翻译成机器语言(hello.o)的过程称为汇编,同时这个机器语言文件也是可重定位目标文件。
作用
汇编就是将高级语言转化为机器可直接识别执行的代码文件的过程,汇编器将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位 目标程序的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件,它包含程序的指令编码。按照平台的不同,CPU的指令集不同,生成的可重定位目标文件的内容也不同。汇编器根据各个平台的指令集逐条语句翻译为二进制的形式存储。同时将一些常量和全局变量插在目标文件的不同的节里。同时,汇编器还会将ELF文件的信息、机器类型等数据写入ELF头,方便后续操作。

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
gcc -c -o hello.o hello.s

4.3 可重定位目标elf格式
命令:readelf -a hello.o > helloo.elf

hello.o的ELF文件helloo.elf由三个部分组成:

  1. ELF头
    ELF头中包含有整个ELF文件的基本信息,包括Magic Number版本、平
    台、程序入口以及各个部分(ELF头、程序头、节头)的大小。
  2. 程序头
    告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序
    头部表,而可重定位目标文件则没有这个头。故显示“本文件中没有程序头。”。
  3. 节头
    包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。可重定位目标文件必须包含节区头部表。
    各部分内容如下:

本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf, sleep,getchar这些符号。重定位节为.rela.text ,一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。

4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

其与hello.s的主要差别如下:
1).分支转移:反汇编代码跳转指令的操作数使用的不是段名称如L1、L2、L3,因为段名称只是在汇编语言中便于编写的一个符号,所以在汇编成机器语言之后显然不存在,而是改为相对偏移的间接地址。
2).立即数表示:hello.s使用立即数皆为10进制表示,而hello.o的反汇编使用立即数皆为16进制表示。
3).函数调用:在hello.s 文件中,函数调用之后直接跟着函数名称,而在反汇编序中,call 的目标地址是当前下一条指令。这是因为 hello.c 中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在反汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其 call 指令后的相对地址设置为全 0,使得目标地址正是下一条指令,然后在.rela.text 节中为其添加重定位条目,等待链接的进一步确定。
4). 全局变量访问:在hello.s 文件中,访问 printf 中的字符串时,使用段名称代号+%rip,在反汇编代码中为 0x0(%rip),因为 rodata 中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全 0 并添加重定位条目。

4.5 本章小结
本章首先介绍了汇编的概念与作用,接下来说明了Ubuntu下汇编的指令,和生成可重定位目标elf格式的指令,给出了可重定位目标elf的三部分内容,对其中的重定位节信息做出了解析。在hello.o的解析过程中,通过比对hello.o反汇编文件和hello.s文件,归纳出了机器语言与汇编语言的映射关系:包括分支转移格式、操作数表示、函数调用格式、全局变量访问格式之间的映射规律。较为详细地了解了机器语言和汇编语言的差异。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
概念:
链接,是指将一个或多个由汇编器生成的二进制存储的可重定位目标文件外加库生成一个二进制的可执行目标文件的过程。链接分为动态链接和静态链接,多数对于公有函数库的引用使用的都是动态链接,可以大大减小目标文件的大小。
可重定位目标文件中存在有一些未定义的符号,以及一些可重定位部分,链接就是解析未定义符号的引用,将目标文件中的占位符替换为符号的地址。
作用:动态链接可以大大减小目标文件的大小,链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。链接是由叫做链接器的程序执行的。链接器使得分离编译成为可能。

注意:这儿的链接是指从 hello.o 到hello生成过程。
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.3 可执行目标文件hello的格式
命令: readelf -a hello > hello.elf
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
hello的ELF文件hello.elf由三个部分组成:
1.ELF头
ELF头中包含有整个ELF文件的基本信息,包括Magic Number版本、平
台、程序入口以及各个部分(ELF头、程序头、节头)的大小。
2.程序头
告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序
头部表,而可重定位目标文件则没有这个头。故显示“本文件中没有程序头。”。
3.节头
包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。可重定位目标文件必须包含节区头部表。

重定位节.rela.text , 是一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。六条重定位信息,分别描述了原hello的函数main、标准头文件的函数puts、函数printf、函数getchar、函数exit、函数sleep的重定位声明。

5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

0x400000为elf头, 在0x4000000x401000段中,程序被载入。在4000000到400fff空间中,与0x4000000x401000段存放的程序相同,在fff之后存放的是.dynamic~.shstrtab节。

5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。

Hello相较于hello.o程序包含以下不同:

  1. 与 hello.o 反汇编文本 helloo.objdump 相比,在 hello.objdump 中多了许多节。如.plt、.init、.fini等。
  2. 函数调用不同,跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址。
  3. 寻址不同,hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于某些地址的定位是不明确的,其地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。
  4. 增加新的函数:
    在hello中链接加入了在hello.c中用到的库函数,如exit、printf、sleep、getchar等函数。

5.6 hello的执行流程
1)载入:_dl_start、_dl_init
2)执行开始阶段:_start、_libc_start_main
3)执行main:_main、_printf、_exit、_sleep、
_getchar、_dl_runtime_resolve_xsave、_dl_fixup、_dl_lookup_symbol_x
4)退出:exit
程序名称 地址
ld-2.27.so!_dl_start 0x7fb85a93aea0
ld-2.27.so!_dl_init 0x7f9612138630
hello!_start 0x400582
lib-2.27.so!__libc_start_main 0x7f9611d58ab0
hello!puts@plt 0x4004f0
hello!exit@plt 0x400530
部分截图:

5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。

对于动态共享链接库中 PIC 函数,编译器需要添加重定位记录,等待动态链接器处理。链接器采用延迟绑定的策略,防止运行时修改调用模块的代码段。
动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
在dl_init调用之前,0x601008 和 0x601010 处的两个 8B 数据为空。对于每一条PIC函数调用,调用的目标地址都实际指向PLT 中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。

在dl_init调用之后,0x601008和0x601010处的两个8B数据分别发生改变为0x7fba 871a8170和0x7fba 86f96680。

其中GOT[1]指向重定位表(另一次运行,不过真的是根据GOT[1]指向的)(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址。

在dl_init调用之后的函数调用时,跳转到PLT执行.plt中逻辑,下一条指令压栈,第一次访问跳转时GOT地址为函数序号,然后跳转到PLT[0]。在PLT[0]中将重定位表地址压栈,然后访问动态链接器。

在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。根据jmp的原理(执行完目标函数之后的返回地址为最近call指令下一条指令地址),之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。

5.8 本章小结
本章首先介绍了链接的概念和作用,给出了链接的命令和可执行目标文件hello的格式,逐步分析了hello的重定位过程、执行过程、动态链接过程。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动过程调用的指令和本地变量。

作用:
进程给每个应用提供了两个抽象:逻辑控制流和私有地址空间。逻辑控制流通过上下文切换让分时处理各个进程,私有地址空间则是通过虚拟内存机制让每个程序仿佛在独占物理内存空间。这样的抽象保证了在不同的情况下运行同样的程序可以得到相同的结果,使得具体的应用不需要关心关心处理器和内存的相关事宜。

6.2 简述壳Shell-bash的作用与处理流程
Shell 的作用:在计算机科学中,Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(命令解析器)。它类似于DOS下的command.com和后来的cmd.exe。它接收用户命令,然后调用相应的应用程序。Shell 是一个用 C 语言编写的程序,他是用户使用 Linux 的桥梁。Shell 是指一种应用程序,Shell 应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
处理流程:
1)从终端读入输入的命令。
2)将输入字符串切分获得所有的参数。
3)如果是内置命令则立即执行。
4)否则调用相应的程序为其分配子进程并运行。
5)shell 应该接受键盘输入信号,并对这些信号进行相应处理。

6.3 Hello的fork进程创建过程
Fork创建:输入命令执行hello后,父进程进行条件判断,如果判断不是内部指令,即会通过fork函数创建子进程。子进程与父进程近似,并得到一份与父进程用户级虚拟空间相同且独立的副本——包括数据段、代码、共享库、堆和用户栈。父进程打开的文件,子进程也可读写。二者之间最大的不同或许在于PID的不同。Fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。
6.4 Hello的execve过程。
execve函数在加载并运行可执行目标文件Hello,且带列表argv和环境变量列表envp。该函数的作用就是在当前进程的上下文中加载并运行一个新的程序。
只有当出现错误时,例如找不到Hello时,execve才会返回到调用程序,这里与一次调用两次返回的fork不同。
在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型:
int main(intargc , char **argv , char *envp);
结合虚拟内存和内存映射过程,可以更详细地说明exceve函数实际上是如何加载和执行程序Hello:

1)删除已存在的用户区域(自父进程独立)。
2)映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的。
3)映射共享区:比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
4)设置PC:exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。

6.5 Hello的进程执行
上下文信息:
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
OS通过处理器调度让处理器轮流执行多个进程,实现不同进程中指令交替执行的机制。处理器的调度等事件会形成异常控制流,上下文切换实现了安全转换。

图为进程的上下文切换。
进程的上下文:
进程的物理实体(代码和数据),和支持进程运行的环境。现场信息:寄存器内容。 (寄存器上下文、硬件上下文)。

进程时间片:
程序在执行过程中,首先会进入主线程(main),运行main方法,当时间到达时间片的时间后,cpu会推出主线程,然后随机选择一个线程(主线程、线程t或是线程myThread),进入到cpu中执行线程,cpu中执行过1ms(一个时间片)后,推出cpu当前线程,重复之前的动作,直到线程执行结束。这个进入cpu的时间段就叫做时间片。
进程调度的过程:
无论是在批处理系统还是分时系统中,用户进程数一般都多于处理机数、这将导致它们互相争夺处理机。另外,系统进程也同样需要使用处理机。这就要求进程调度程序按一定的策略,动态地把处理机分配给处于就绪队列中的某一个进程,以使之执行。
进程调度有以下两种基本方式:

  1. 非剥夺方式
    分派程序一旦把处理机分配给某进程后便让它一直运行下去,直到进程完成或发生某事件而阻塞时,才把处理机分配给另一个进程。
  2. 剥夺方式
    当一个进程正在运行时,系统可以基于某种原则,剥夺已分配给它的处理机,将之分配给其它进程。剥夺原则有:优先权原则、短进程、优先原则、时间片原则。
    例如,有三个进程P1、P2、P3先后到达,它们分别需要20、4和2个单位时间运行完毕。
    假如它们就按P1、P2、P3的顺序执行,且不可剥夺,则三进程各自的周转时间分别为20、24、
    26个单位时间,平均周转时间是23.33个时间单位。
    假如用时间片原则的剥夺调度方式,可得到:
    可见:P1、P2、P3的周转时间分别为26、10、6个单位时间,平均周转时间为14个单位时间。
    用户态与核心态转换:

一般来说,一个进程可以在两种状态下执行,用户模式和内核模式,内核模式的权限高于用户模式的权限。进程每次调用一个系统调用时,进程的运行方式都发生变化:从用户模式切换到内核模式,然后继续执行。当出现系统调用或者发生异常时,进程就会从用户态切换回内核态。如果一个进程进入内核模式,那1么那个进程就不会被进程调度切换,而会一直占有CPU,直到该进程终止或退出内核模式准备进入用户模式。
进程hello可能会由于用户按下ctrl+z或者ctrl+c向进程发送信号进入内核模式,或者出现缺页异常而进入内核模式,启动异常处理程序,如果是ctrl+z引发异常,异常处理程序将挂起hello进程,接着进行进程调度切换至其它进程。如果是ctrl+c,异常处理程序将直接终止进程。如果产生缺页异常,异常处理程序将进行系统调用,在磁盘上读取相应的虚拟页,并将其装入内存,在从内核模式退出时,将进行系统进程调度,切换到其它进程,当再次切换回hello时,将从上次导致缺页异常的地方再次执行,这次则不会引发缺页异常,进程正常地执行下去。

6.6 hello的异常与信号处理
异常类型:
类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令
终止 不可恢复的错误 同步 不会返回

异常处理:
1)中断

2)陷阱

  1. 故障

  2. 终止

程序执行:
乱按:

乱按输入的字符串会被getchar读取,等到程序运行结束后把输入的字符串打印。
按ctrl-c:

进程收到SIGINT命令行,发送终止信号进程结束。

按ctrl+z:
进程收到SIGSTP信号,将程序暂时挂起。

获取其job号为1,使用fg 1命令行将其调到前台,会继续打印之前剩下的语句。

输入pstree命令行:

输入kill命令杀死进程:

6.7本章小结
本章首先说明了进程的概念和作用,接着简述了壳Shell-bash的作用与处理流程,接下来说明了fork和excuve的原理和作用,然后从上下文、时间片、调用流程、用户态内核态转换等方面讲述了hello的执行过程。接着尝试在执行hello程序时键入各种命令,观察进程和信号之间的联系。总体来看很全面地说明了hello程序的异常和信号处理。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址
逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。如程序hello产生的与段相关的偏移地址部分。
线性地址
跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。
虚拟地址
和线性地址等同。
物理地址
用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性地址就直接成为物理地址了。

7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,如图:

索引段,是段描述符的索引,段描述符的具体地址描述了一个段。这样,很多个段描述符,就组了一个数组,叫“段描述符表”可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。

这里的Base字段,描述了一个段的开始位置的线性地址。Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT。
GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。

首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],
1)看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
2)拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
3)把Base + offset,就是要转换的线性地址了。
7.3 Hello的线性地址到物理地址的变换-页式管理
CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。从管理和效率的角度出发,线性地址被分为以固定长度为单位的组,称为页,例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个220个元素的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址——对应的页的地址。另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。这里注意到,这个数组有220个成员,每个成员是一个地址(32位机,一个地址也就是4字节),那么要单单要表示这么一个数组,就要占去4MB的内存空间。为了节省空间,引入了一个二级管理模式的机器来组织分页单元。

如上图,
1)分页单元中,页目录是唯一的,它的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。
2)每一个活动的进程,因为都有其独立的对应的虚拟内存(页目录也是唯一的),那么它也对应了一个独立的页目录地址。——运行一个进程,需要将它的页目录地址放到cr3寄存器中,将别个的保存下来。
3)每一个32位的线性地址被划分为三部分,页目录索引(10位),页表索引(10位),偏移(12位)
依据以下步骤进行转换:
1)从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
2)根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
3)根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
4)将页的起始地址与线性地址中最后12位相加,得到最终我们想要的地址。

按教材中的解释,二级模式空间的节约是从两个方面实现的:
A、如果一级页表中的一个页表条目为空,那么那所指的二级页表就根本不会存在。这表现出一种巨大的潜在节约,因为对于一个典型的程序,4GB虚拟地址空间的大部份都会是未分配的;
B、只有一级页表才需要总是在主存中。虚拟存储器系统可以在需要时创建,并页面调入或调出二级页表,这就减少了主存的压力。只有最经常使用的二级页表才需要缓存在主存中。——不过Linux并没有完全享受这种福利,它的页表目录和与已分配页面相关的页表都是常驻内存的。
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。
多级页表:
将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。

解析VA,利用前m位vpn1寻找一级页表位置,接着一次重复k次,在第k级页表获得了页表条目,将PPN与VPO组合获得PA。

7.5 三级Cache支持下的物理内存访问
CPU发送一条虚拟地址,随后MMU按照上述操作获得了物理地址PA。根据cache大小组数的要求,将PA分为CT(标记位)CS(组索引),CO(偏移量)。根据CS寻找到正确的组,比较每一个cacheline是否标记位有效以及CT是否相等。如果命中就直接返回想要的数据,如果不命中,就依次去L2,L3,主存判断是否命中,当命中时,将数据传给CPU同时更新各级cache的cacheline(如果cache已满则要采用换入换出策略)。

7.6 hello进程fork时的内存映射
内存映射,是指一个虚拟内存空间和一个磁盘上的对象之间的映射关系。内存映射分为两种:文件映射和匿名映射,文件映射将一个普通文件的全部或者一部分映射到进程的虚拟内存中,映射后,进程就可以直接在对应的内存区域操作文件内容;匿名映射没有对应的文件或者对应的文件时虚拟文件,映射后会把内存分页全部初始化为0。如果多个进程都会同一个内存区域操作时,会根据映射的特性,会有不同的行为。
内核调用fork函数时主要涉及的内存映射主要是虚拟地址空间到物理地址空间的映射。当多个进程映射了同一个内存区域时,它们会共享物理内存的相同分页。根据映射特征可分为私有映射和共享映射:私有映射的映射的内容对其他进程不可见,而共享映射中某一个进程对共享的内存区域操作都对其他进程可见。
当一个进程调用fork函数创建新的子进程时,内核在虚拟内存上分配一个空间,并且完全复制父进程的映射,即子进程和父进程的虚拟地址空间都映射到了同一个物理地址空间。然而,两个进程的每个区域结构都标记为私有的。这样,在父进程或者子进程在对内存数据进行修改时,会涉及一个技术:写时复制技术。当子进程或者父进程对页进行修改时,子进程就会在物理内存上创建新的页,并映射虚拟内存上的对应页到新的物理页,这样就可以保证子进程和父进程的地址空间私有。

7.7 hello进程execve时的内存映射
当一个进程调用execve函数后,由于需要放弃继承自父进程的物理地址空间,使用独有的物理地址空间,内核将这部分与父进程共享的物理内存对应的虚拟内存取消映射,并将该部分虚拟内存释放掉。
接着内核读取hello的ELF头,按照节头信息将各个节装入虚拟内存中(需要时分配空间)。在物理内存中分配一个区域,将虚拟内存中进程的代码段和数据段映射到新分配的区域,并将映射标记为私有映射,真正的将代码和数据装入物理内存是在缺页时。

7.8 缺页故障与缺页中断处理
缺页故障是一种常见的故障,当指令引用一个虚拟地址,在 MMU 中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就会发生故障。
缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU 重新启动引起缺页的指令,这条指令再次发送 VA 到MMU,这次 MMU 就能正常翻译 VA 了。

7.9动态存储分配管理
动态内存管理的基本方法与策略:
1)动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。
2)每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。
3)空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。
4)一个已分配的块保持已分配状态,直到它被释放。

两种分配器:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。

带边界标签的隐式空闲链表
1)堆及堆中内存块的组织结构:

在内存块中增加4字节的头部(用于寻找下一个块)和4字节的脚部(用于寻找上一个块)。脚部的设计是专门为了合并空闲块更方便。因为头部和脚部大小已知,所以利用头部和脚部中存放的块大小就可以寻找上下块。
2)隐式链表
对比于显式空闲链表,隐式空闲链表代表并不直接对空闲块进行链接,而是将对内存空间中的所有块组织成一个大链表。隐式空闲链表中头部和脚部中的块大小间接起到了前驱、后继指针的作用。
3)空闲块合并
可以利用脚部方便的对前面的空闲块进行合并。合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。只需要通过改变头部和脚部中的值,就可以完成对于四种情况分别进行空闲块合并。
显式空间链表
将空闲块组织成链表形式的数据结构。堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个 pred(前驱)和 succ(后继)指针,如图:

7.10本章小结
本章首先介绍了四种地址:物理地址、逻辑地址、虚拟地址和线性地址。接着说明了段式管理和页式管理的内容,介绍了逻辑地址怎样一步步寻址为物理地址。介绍了空间节约的方法:多级页表,并说明了四级页表下的转化过程,介绍了进程执行fork和excuve时的映射,说明了缺页故障的概念和缺页中断处理,在最后介绍了动态内存分配两种分配:隐式和显式及其相关特点。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备文件作为设备驱动程序的门户,负责数据在操作系统和设备间的中转。数据从应用程序或操作系统传递到设备文件,然后设备文件将它传递给设备驱动程序,驱动程序再将它发给物理设备。反向的数据通道也可以用,从物理设备通过设备驱动程序,再到设备文件,最后到达应用程序或其他设备。
由于设备已被模型化为文件,于是对设备的数据操作就可以使用操作系统的IO接口来完成。

设备管理:unix io接口
所有的 IO 设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。我们可以对文件的操作有:打开关闭操作open和close;读写操作read和write;改变当前文件位置lseek等。

8.2 简述Unix IO接口及其函数
Unix I/O 接口统一操作:
1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备。内核返回一个小的非负整数,叫做描述符。描述符在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
2)Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
3)改变当前的文件位置:内核保持着每个打开的文件的一个文件位置k。k初始为0。这个文件位置k表示的是从文件开头起始的字节偏移量。应用程序能够通过执行seek,显式地将改变当前文件位置 k,例如各种fread或fwrite。
4)读写文件:
读操作就是从文件复制n>0个字节到内存。从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的文件。当k>=m时,触发EOF。
写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k=k+n。
5)关闭文件:内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

Unix I/O 函数:
1)int open(char* filename,int flags,mode_t mode) ,进程通过调用open函数来打开一个存在的文件或是创建一个新文件的(即fopen的内层函数)。open函数将filename(文件名,含后缀)转换为一个文件描述符(C中表现为指针),并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件(读或写或两者兼具),mode参数指定了新文件的访问权限位(只读等)。
2)int close(fd),fd是需要关闭的文件的描述符(C中表现为指针),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的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
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;

}

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);
}

vsprintf 按照格式 fmt 结合参数 args 生成格式化之后的字符串,并返回字符串的长度。其主要作用就是格式化,用格式字符串对个数变化的参数进行格式化,产生格式化输出。
write实现:
write,写操作,把buf中的i个元素的值写到终端。其实现如下
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
在 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

ecx中是要打印出的元素个数 ;ebx中的是要打印的buf字符数组中的第一个元素;这个函数的功能就是不断的打印出字符,直到遇到:’\0’终结符 ;[gs:edi]对应的是0x80000h:0采用直接写显存的方法显示字符串。
syscall 将字符串中的字节“Hello 1190201709 杨忠博”从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB 分量)。于是字符串“Hello 1190201709 杨忠博”就显示在了屏幕上。
8.4 getchar的实现分析
工作原理:
getchar()是stdio.h中的库函数,它的作用是从stdin流中读入一个字符,也就是说,如果stdin有数据的话不用输入它就可以直接读取了,第一次getchar()时,确实需要人工的输入,但是如果你输了多个字符,以后的getchar()再执行时就会直接从缓冲区中读取了。
实际上是 输入设备->内存缓冲区->getchar()
你按的键是放进缓冲区了,然后供程序getchar()
键盘输入的字符都存到缓冲区内,一旦键入回车,getchar就进入缓冲区读取字符,一次只返回第一个字符作为getchar函数的值,如果有循环或足够多的getchar语句,就会依次读出缓冲区内的所有字符直到’\n’.要理解这一点,之所以你输入的一系列字符被依次读出来,是因为循环的作用使得反复利用getchar在缓冲区里读取字符,而不是getchar可以读取多个字符,事实上getchar每次只能读取一个字符.如果需要取消’\n’的影响,可以用getchar();来清除,这里getchar();只是取得了’\n’但是并没有赋给任何字符变量,所以不会有影响,相当于清除了这个字符。
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成 ASCII 码,保存到系统的键盘缓冲区之中。

getchar调用read等系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章首先说明了linuxIO设备的管理方法,接着简述了UnixIO接口及其函数,然后从printf、vsprintf、write、syscall等源码或实现方式的角度讲述了printf的底层实现过程,最后是对字符串读取方法getchar的简要分析。
(第8章1分)
结论
最经典且简短的程序hello的一生就这样度过了,一瞬的时间里,hello却经历了很多复杂的变换:
P2P阶段:
编写:通过editor将代码键入 hello.c
预处理:将hello.c调用的所有外部的库展开,所有的宏定义替换,合并到一个hello.i文件中
编译:将 hello.i 编译成为汇编文件 hello.s
汇编:将 hello.s 会变成为可重定位目标文件 hello.o
链接:将 hello.o 与可重定位目标文件和动态链接库链接成为可执行目 标程序 hello
020阶段:
运行:在shell(terminal)中输入./hello 1190201709 杨忠博
创建子进程:shell父进程调用fork函数为hello创建子进程
运行程序:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序,开始载入物理内存,然后CPU进入main函数执行程序。
执行指令:CPU为hello分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流
访问内存:MMU将程序中使用的虚拟内存地址,通过页表映射成物理地址。
动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
键入信号:途中键入ctr-c、ctr-z,则调用shell的信号处理函数分别停止、挂起。
结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。

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

附件
列出所有的中间产物的文件名,并予以说明起作用。

中间结果文件:
1.hello.i 预处理之后的文本文件
2.hello.s 汇编后的汇编文件
3.hello.o 汇编后的可重定位执行文件
4.hello 链接后的可执行文件
5.hello.elf hello的elf格式
6.helloo.elf hello.o的elf格式
7.hello.objdump hello的反汇编
8.helloo.objdump hello.o的反汇编

(附件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分)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值