程序人生-Hello’s P2P

第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 -

第1章 概述
1.1 Hello简介
P2P:
hello的源码hello.c的源文件,要生成可执行文件首先要进行预处理:预处理器根据以#开头的命令,修改初原始的C程序,生成hello.i文件;其次要进行编译,将文本文件hello.i翻译成文本文件hello.s;接着进行汇编,将hello.s翻译成机器语言指令,把这些指令打包成可重定位的目标程序,并打包在文件hello.o中。最后进行链接,由链接器将可重定位的目标文件进行合并,生成可执行文件。
020:
可执行文件需要执行环境,它可以在linux下通过shell进行运行,与计算机其他可执行文件同步与运行,并通过异常处理机制对响应信号进行处理。shell执行hello后为其映射虚拟内存,随后载入物理内存开始执行hello的程序,将其输出到屏幕,随后结束进程。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;1GHz;16G RAM;256G GHD Disk
1.2.2 软件环境
Windows10 64位;VMware Workstation 15.5;Ubuntu 18.04 LTS
1.2.3 开发工具
Visual Studio Code 2019;Code::Blocks IDE;gcc;gdb;objdump;readelf
1.3 中间结果

  1. hello.i hello.c预处理之后的文件
  2. hello.s hello.i编译成汇编语言后的文件
  3. hello.o hello.s生成的二进制文件
  4. hello hello的可执行文件
  5. hello_1.txt 用objdump对hello.o反汇编的结果
  6. hello_2.txt 用objdump对hello反汇编的结果
  7. hello_3.txt 对hello用readelf生成的elf表
  8. hello_4.txt 对hello.o用readelf生成的elf表
    1.4 本章小结
    本章主要介绍了hello的P2P,020的整个过程以及实验的环境、工具和中间产物。

第2章 预处理
2.1 预处理的概念与作用
预处理一般指由预处理器对程序源代码进行处理的过程。预处理器(cpp)根据字符#开头的命令,修改原始的C程序。比如hello.c中第一行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。
作用:

  1. 宏定义: 将宏名替换为文本: 字符串或者代码。
  2. 文件包含: cpp将查找指定的被包含头文件, 并将其复制插入到#include命令出现的位置上。
  3. 条件编译: 有些语句希望在条件满足时才编译, 预处理过程中根据条件决定需要编译的代码。
    2.2在Ubuntu下预处理的命令

图2.2
2.3 Hello的预处理结果解析
可以看出, 预处理以后文件是一个3118的c程序文件, 其中3098行后的内容对应hello.c中第10行之后的内容.之前的内容是头文件stdio.h unistd.h stdlib.h被复制并被插入到#include命令出现的位置上产生的。被包含的文件还可能还有其他文件, 因此该过程可能是嵌套进行的。其中标志出了很多可能用到的函数:

以及库的位置等:

图2.3.1

图2.3.2
2.4 本章小结
本章简单介绍了c语言在编译前的预处理过程, 简单介绍了预处理过程的概念和作用, 对预处理过程进行了演示, 并距离说明了预处理的结果还有解析预处理的过程。

第3章 编译
3.1 编译的概念与作用
概念:
利用编译程序把由高级语言源程序转化为汇编语言程序的过程。
过程:
(1) 词法分析: 将源代码程序输入扫描器, 将源代码的字符序列分割成一系列记号。
(2) 语法分析: 基于词法分析得到的一系列记号, 生成语法树。
(3) 语义检查: 由语义分析器完成, 指示判断是否合法, 并不判断对错。
(4) 中间代码生成: 中间代码 (语言) 使得编译器分为前端和后端, 前端产生与机器 (或环境) 无关的中间代码, 编译器的后端将中间代码转换为目标机器代码。目的: 一个前端对多个后端, 适应不同的平台。
(5) 代码优化: 代码生成器依赖于目标机器, 依赖目标机器的不同字长, 寄存器, 数据类型等。
(6) 目标代码生成: 目标代码优化器选择合适的寻址方式, 左移右移代替删除, 删除多于指令。
作用:
把代码翻译成汇编语言。注意, 这里的编译是指从.i到.s即预处理后的文件到生成汇编语言程序。
3.2 在Ubuntu下编译的命令

图3.2
3.3 Hello的编译结果解析
3.3.1数据
1. int sleepsecs:
sleepsecs是一个全局的int型变量, 由于已经初始化过了, 所以存储在.data段中. 由于sleepsecs为int类型, 赋值时发生了隐式类型的转换. 该变量的类型为object, 占用的空间为4个字节, 设置为long类型, 其值为2

图3.3.1

	2. int i:

i是一个局部变量. 局部变量一般存储在寄存器或者堆栈中. 由i的赋值语句可以看出i存放的位置是-4(%rbp):

图3.3.2

	3. 字符串:
		程序中有两个字符串: “Usage: Hello 学号 姓名!\n”和“Hello %s %s\n”

都存在只读数据段中.

图3.3.3
4. 数组
程序中有一个数组argv[]. argv[1]和argv[2]作为for循环中printf的参数.
由该循环的汇编语句可以知道argv的首地址是-32

图3.3.4
5. 其他数据为立即数

3.3.2赋值
1. int sleepsecs = 2.5:
sleepsecs是全局变量, 在.data中被赋值.

	2. int i = 0:

图3.3.5

此语句的意思是将一个双字的立即数0传送到%rbp – 4所指向的内存单元.
3.3.3类型转换
将float类型的2.5赋值给int类型的全局变量sleepsecs时发生了强制类型转换, 2.5被向下取整为2.
3.3.4算术操作
程序中出现的算数操作为:

图3.3.4.1

	在.s文件中被编译为:

图3.3.4.2

该语句的意思是: 将%rbp – 4所指向的内存中的数与立即数1相加, 并将所得的和传送到%rbp – 4所指向的内存地址.
3.3.5关系操作与控制转移
程序中的控制转移通过比较 + 跳转实现.

图3.3.5.1
被编译为:

图3.3.5.2
代表的意思是用%rbp – 20所指向的内存中的数与立即数3相减, 并设置条件码, 若条件码ZF为0, 则跳转至.L2, 若不为0, 则不跳转.

图3.3.5.3
中的i < 10
被编译为:

图3.3.5.4

代表的意思是用%rbp – 4所指向的内存中的数与立即数9相减, 并设置条件码, 若条件码SF ^ OF为1, 则跳转至.L4 若为0, 则不跳转.
3.3.6数组/指针/结构操作
循环体

图3.3.6.1

中的数组元素的操作被编译成:

图3.3.6.2
程序对argv[1]和argv[2]的寻址被编译为基址+偏移的寻址方式.
由前面的分析得知: argv的首地址存放在-32(%rbp)的位置, 由于每一个数据元素的字长为8个字节, 所以首地址+8得到argv[1]的地址, 首地址+16得到argv[2]的地址.
3.3.8函数操作
函数操作调用包括以下几个步骤:
(1) 参数传送: 在x86-64中, 大部分过程间的数据传送是通过寄存器实现的, 通过寄存器最多可以传递6个整型(例如整数和指针)参数. 超出6个的部分就要通过栈来传递.

(2) 返回地址入栈: 将当前代码区调用指令的下一条指令地址压入栈中, 供函数返回时继续执行.

(3) 代码区跳转: 处理器从当前代码区跳转到被调用函数的入口处.

(4) 栈帧调整, 包括:
1) 保存当前栈帧状态值,已备后面恢复本栈帧时使用(rbp入栈)
2) 将当前栈帧切换到新栈帧(将rsp值装入rbp,更新栈帧底部)
3) 给新栈帧分配空间(把ESP减去所需空间的大小,抬高栈顶)

	函数返回的步骤如下:
		(1) 保存返回值,通常将函数的返回值保存在寄存器rax中.

		(2) 弹出当前帧,恢复上一个栈帧, 具体包括:
  1. 在堆栈平衡的基础上,给rsp加上栈帧的大小,降低栈顶,回收当前栈帧的空间。

  2. 将当前栈帧底部保存的前栈帧rbp值弹入rbp寄存器,恢复出上一个栈帧。

  3. 将函数返回地址弹给rip寄存器.

     	(3) 跳转:按照函数返回地址跳回母函数中继续执行.
    
     程序中主要调用以下几个函数:
     	1. main函数: 
     		被系统启动函数__libc_start_main调用.
    

图3.3.8.1

可以看出第一个参数被存储在edi中, 而第二个参数被存储在rsi中

		2. printf函数:
			第一次调用:

图3.3.8.1
被编译为:

图3.3.8.2

			第一个参数被压入了rdi中, 也就是字符串的首地址.

第二次调用:

图3.3.8.3

被编译为:

图3.3.8.4

由于第二次printf中除了有字符串常量, 还包括argv[1]和argv[2]两个参数故依次将其存储在%rsi %rdx中, 再将字符串首地址传到rdi中, 最后再call printf

		3. exit函数:

图3.3.8.5

			被编译为:

图3.3.8.6

			向edi中传递参数1, 再call exit
  1. sleep函数:

图3.3.8.7
被编译为:

图3.3.8.8

将sleepsecs传递给edi作为参数, 执行sleep函数.
  1. getchar函数:

图3.3.8.9

被编译为:

图3.3.8.10

	本身不需要参数, 直接call getchar即可

3.4 本章小结
本章通过编译器对上一章生成的.i文件编译成.s文件,并对数据类型,赋值,类型转换,算数操作,关系操作,位移控制,函数进行了分析,将编译过程进行了全面的分析,并对一些常用的汇编指令进行了阐述。
编译器由.i文件处理成了.s文件的汇编代码,现在我们对汇编代码也有了初步的了解可以基本看懂汇编语言并进行分析。

第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中.
作用: 将汇编代码转变为机器指令, 生成目标文件.
4.2 在Ubuntu下汇编的命令

图4.2.1
4.3 可重定位目标elf格式

  1. ELF头:
    ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,节头部表中条目的大小和数量等。

图4.3.1

  1. 节头表
    记录每个节的名称, 类型, 属性(读写权限), 在ELF文件中所占的长度, 对齐方式和偏移量

图4.3.2
3. 重定位节
重定位条目告诉链接器在目标文件合并成可执行文件时如何修改这个引用. 如图, offset是需要被修改的引用的节的偏移, 符号标识被修改引用应该指向的符号. type告知连接器如何修改新的引用, 加数是一个有符号常数, 一些类型的重定位要用它对被修改引用的值做偏移调整.

图4.3.3

	重定位类型分为两种: 
  1. R_X86_64_PC32: 重定位一个使用32位PC相对地址的引用。在指令中编码的32位值加上PC的当前运行时值,得到有效地址。
    运用公式: refaddr = ADDR(s) + r.offset
    *refptr = (unsigned)(ADDR(r.symbol) + r.addend – refaddr)

  2. R_X86_64_32: 重定位一个使用32位PC绝对地址的引用。直接使用在指令中编码的32位值作为有效地址。
    运用公式: *refptr = (unsigned)(ADDR(r.symbol) + 0 )

     其中, ELF重定位条目为:
    

图4.3.4

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

图4.3.5

4. 符号表

它存放程序中定义和引用的函数和全局变量的信息, 符号表不包括局部变量的条目.

图4.3.6
4.4 Hello.o的结果解析
1.与hello.s相比较除了头尾部不同外, 反汇编代码与hello.s差别不大.
2.hello.s使用十进制, 反汇编代码中使用16进制.
3.hello.s中使用到的.L2, .L4, .L3标识符都用了地址代替, 从而在分支转移部分全部都是jmp到相对main函数起始地址的偏移表示的跳转的地址.
4.hello.s中对函数调用均用的是函数名; 反汇编中函数调用的目标地址是当前的下一条指令. 在call后的地址为全0, 在重定位节中有对应的重定位条目, 链接之后确定地址.
5.hello.s中使用段名称和%rip访问, 反汇编代码中使用0x0(%rip)访问. 机器码中待访问的全局变量地址为全0, 重定位节中有对应的重定位条目, 链接之后确定地址.

图4.4.1
4.5 本章小结
本章简单描述了从hello.s到hello.o的过程,着重介绍了重定位,同时通过比较反汇编代码和hello.s的不同,更加明确的阐释了汇编的作用,以及在汇编过程中对代码所进行的操作.

第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.3 可执行目标文件hello的格式

  1. ELF头

图5.3.1

  1. 节头表

图5.3.2

节头表内容如图, 包含了各个节的大小以及各个节的位置.
其中:

.text节是保存了程序代码指令的代码节. 由于.text节保存了程序代码,因此节的类型为SHT_PROGBITS.
.rodata保存只读数据.类型SHT_PROGBITS.
.plt 过程链接表,包含动态链接器调用从共享库导入的函数所必须的相关代码.存在于text段中,类型SHT_PROGBITS.
.bss节保存未初始化全局数据,是data的一部分.程序加载时数据被初始化成0,在程序执行期间可以赋值,未保存实际数据,类型SHT_NOBITS.
.got节保存全局偏移表.它和.plt节一起提供了对导入的共享库函数访问的入口.由动态链接器在运行时进行修改.类型SHT_PROGBITS。
.dynsym节保存共享库导入的动态符号信息,该节在text段中,类型SHT_DYNSYM.
.dynstr保存动态符号字符串表,存放一系列字符串,代表了符号的名称,以空字符作为终止符.
.rel节保存重定位信息,类型SHT_REL.
.hash节,也称为.gnu.hash,保存一个查找符号散列表.
.symtab节,保存了ElfN_Sym类型的符号信息,类型SHT_SYMTAB.
strtab节,保存符号字符串表,表中内容被.symtab的ElfN_Sym结构中的st_name条目引用。类型SHT_SYMTAB.
.shstrtab节,保存节头字符串表,以空字符终止的字符串集合,保存了每个节节名,如.text,.data等。有个e_shsrndx的ELF文件头条目会指向.shstrtab节,e_shstrndx中保存了.shstrtab的偏移量.这节的类型是SHT_SYMTAB.
.ctors和.dtors节,前者构造器,后者析构器,指向构造函数和析构函数的函数指针,构造函数是在main函数执行前需要执行的代码,析构是main函数之后需要执行的代码.

3. 程序头表

图5.3.3
程序包含八个type:

  1. PTDR: 指定程序头表在文件及程序内存映像中的位置和大小.
  2. INTERP: 指定要作为解释程序调用的以空字符结尾的路径名的位置和大小.对于动态可执行文件,必须设置此类型。
  3. LOAD: 指定可装入段,通过p_filesz和p_memsz进行描述.文件中的字节会映射到内存段的起始位置.
  4. DYNAMIC: 指定动态链接信息.
  5. NOTE: 指定辅助信息的位置和大小.
  6. GNU_STACK: 权限标志,标志栈是否是可执行的.
  7. GNU_RELRO: 指定在重定位结束之后那些内存区域是需要设置只读.
    5.4 hello的虚拟地址空间
    (1) .PDHR: 起始位置为0x400040, 大小为0x1c0

图5.4.1

(2) .INTERP: 起始位置为0x400200, 大小为0x1c

图5.4.2

(3) .LOAD: 起始位置为: 0x400000, 大小为0x76c

图5.4.3

(4) .LOAD: 起始位置为: 0x600e50, 大小为0x1f8

图5.4.4

(4) .DYNAMIC: 起始位置为: 0x600e50, 大小为0x1a0

图5.4.5

(5) .NOTE: 起始位置: 0x40021c, 大小为0x20

图5.4.6

(6) .GNU_STACK: 起始位置: 0x0, 大小为0












(7) .GNU_RELRO: 起始位置: 0x600e50, 大小为1b0

图5.4.7

5.5 链接的重定位过程分析

  1. hello中增加了许多节和被调用的函数.

图5.5.1

2.引用了rodata中的数据, 而在hello.o中printf参数的字符串用全0代替.
3. hello中使用的都是确定的地址, 这是因为链接后全局变量的地址能够确定.

图5.5.2

  1. hello.o中的地址从0开始, 而hello中main的地址不是0开始
    hello.o:

图5.5.3

hello:

图5.5.4

  1. hello中有库函数的代码

图5.5.5
5.6 hello的执行流程
程序名称 程序地址
ld-2.27.so!_dl_start 0x00007f087b8349b0
ld-2.27.so!_dl_setup_hash 0x00007f742928ca50
ld-2.27.so!_dl_sysdep_start 0x00007f742929a210
ld-2.27.so!strlen 0x00007f742929dfd0
ld-2.27.so!sbrk 0d00007fa1fc5ff500
ld-2.27.so!_dl_next_ld_env_entry 0x00007fa1fc5fed40
ld-2.27.so!_dl_new_object 0x00007fa1fc5f0b90
ld-2.27.so!calloc@plt
0x00007fa1fc5e5a90
ld-2.27.so!memcpy 0x00007fa1fc602f60
ld-2.27.so!_dl_add_to_namespace_list 0x00007fa1fc5f0b00
ld-2.27.so!strcmp 0x00007fa1fc600b40
ld-2.27.so!_dl_new_object 0x00007fa1fc5f0b90
ld-2.27.so!malloc@plt
0x00007fa1fc5e5a80
ld-2.27.so!_dl_add_to_namespace_list 0x00007fa1fc5f0b00
ld-2.27.so!_dl_discover_osversion 0x00007fa1fc5feb70
ld-2.27.so!memcmp 0x00007fa1fc6026e0
ld-2.27.so!_dl_init_paths 0x00007fa1fc5ed4e0
ld-2.27.so!_dl_important_hwcaps 0x00007fa1fc5f41c0
ld-2.27.so!access 0x00007fa1fc6003c0
ld-2.27.so!memcpy 0x00007fa1fc602d30
ld-2.27.so!memset 0x00007fa1fc602c40
ld-2.27.so!_dl_debug_initialize 0x00007fa1fc5f6070
ld-2.27.so!_dl_count_modids 0x00007fa1fc5f8260
ld-2.27.so!_dl_debug_state 0x00007fa1fc5f6060
ld-2.27.so!_dl_map_object_deps 0x00007fa1fc5f2f80
ld-2.27.so!strchr 0x00007fa1fc600920
ld-2.27.so!_dl_catch_error 0x00007fa1fc5f54f0
ld-2.27.so!_sigsetjmp 0x00007fa1fc600610
ld-2.27.so!_dl_name_match_p 0x00007fa1fc5f6980
ld-2.27.so!_dl_sysdep_read_whole_file 0x00007fa1fc5f66f0
ld-2.27.so!open64 0x00007fa1fc600360
ld-2.27.so!_fxstat 0x00007fa1fc6002e0
ld-2.27.so!mmap64 0x00007fa1fc600480
ld-2.27.so!close 0x00007fa1fc600460
ld-2.27.so!_dl_cache_libcmp 0x00007fa1fc5fd190
ld-2.27.so!init_tls 0x00007fa1fc5e5ac0
ld-2.27.so!_dl_relocate_object 0x00007fa1fc5f1270
ld-2.27.so!_dl_init 0x00007fa1fc5f5740
hello!_start 0x0000000000400480
hello!puts@plt 0x0000000000400460
hello!exit@plt 0x00000000004004a0

5.7 Hello的动态链接分析
程序调用一个由共享库定义的函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。GNU编译系统使用延迟绑定的技术解决这个问题,将过程地址的延迟绑定推迟到第一次调用该过程时。
延迟绑定要用到全局偏移量表(GOT)和过程链接表(PLT)两个数据结构。如果一个目标模块调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT。
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,跳转到动态链接器中。每个条目都负责调用一个具体的函数。PLT[1]调用系统启动函数 (__libc_start_main)。从PLT[2]开始的条目调用用户代码调用的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在ld-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。
分析:

图5.6.1

节头表中找到GOT的起始地址为601000, 偏移量为0x1000
调用_dl_start之前有:

图5.6.2
调用之后有:

图5.6.3
GOT[2]是动态链接器在ld-linux.so模块中的入口点, 共享库模块:

图5.6.4
第一次调用puts前的跳转地址:

图5.6.5
调用puts之后的跳转地址:

图5.6.6

说明调用printf前未链接到动态库, 调用后链接到了动态库, 说明是动态链接的, 可以跳到确切的地址.

5.8 本章小结
hello.o经过一系列链接变成可执行文件hello, 而本章中对于链接过程中的重定位进行了详细的介绍, 也展现了重定位在链接过程中的重要性.
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程是一个执行中程序的实例。是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
作用:进程的概念为我们提供了两个关键的抽象:
(1)一个独立的逻辑控制流:他提供一个假象,好像我们的程序在独占地使用处理器。
(2)一个私有的地址空间:他提供一个假象,好像我们的程序在独占地使用内存系统。
处理器好像是无间断地一条接一条地执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell俗称壳,是一个交互型的应用级程序,是指“为使用者提供操作界面”的软件(命令解析器)。它接收用户命令,然后调用相应的应用程序。
1.功能:命令解释;可执行文件都可以作为Shell命令来执行。
2.处理流程:
1)当用户提交了一个命令后,Shell首先判断它是否为内置命令,如果是就通过Shell内部的解释器将其解释为系统功能调用并转交给内核执行。
2)若是外部命令或应用程序就试图在硬盘中查找该命令并将其调入内存,再将其解释为系统功能调用并转交给内核执行。
6.3 Hello的fork进程创建过程
在shell中输入./hello,shell判断不是内置命令,通过fork函数创建一个新的运行的子进程来加载并运行可执行文件hello。新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、用户库以及用户栈。子进程还可以获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件,父进程和新创建的子进程之间最大的区别在于它们有不同的PID。fork被调用一次,返回两次。在父进程中fork返回子进程的pid,在子进程中fork返回0.父进程与子进程是并发运行的独立进程。
6.4 Hello的execve过程
execve函数在新创建的子进程的上下文中加载并运行hello程序,且带参数列表argv和环境变量列表envp。只有发生错误时execve才会返回到调用程序。execve调用一次且从不返回。在execve加载hello之后调用启动代码,启动代码设置栈并将控制传递给新程序的主函数。
当main开始执行时,用户栈组织结构如图:

图6.4.1
6.5 Hello的进程执行
逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流使用处理器的。
调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程的决定。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。(总的来说就是权限的问题)
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
通过上下文切换,控制流从一个进程传递到另外一个进程。
执行sleep时:

图6.5.1

在hello进程调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时时发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。

调用getchar:
内核可以执行上下文切换,将控制转移到其他进程。getchar()的数据传输结束之后,引发一个中断信号,控制回到hello进程中。

图6.5.2
6.6 hello的异常与信号处理
异常的类别:

图6.6.1

信号:

图6.6.3
信号的处理:
当内核把进程p从内核模式切换到用户模式时,它会检查进程p的未被阻塞的待处理信号的集合。如果集合非空,内核强制p接收信号k。收到这个信号会触发进程采取某种行为,一旦完成行为,控制就传递回p的逻辑控制流中的下一条指令,每个信号类型都有一种默认行为,可以通过设置signal函数改变和信号signum相关联的行为。

  1. 正常退出:

图6.6.4

程序结束后,被正常回收

2.随便乱按:

图6.6.5

乱按会把输入的数留在输入缓冲区,作为接下来的命令行输入。同时程序可以被正常回收。



3.ctrl+c:

图6.6.6

这个操作向进程发送一个sigint信号,让进程直接结束,信号处理程序会回收子进程。可以看到hello进程已被终止,后台没有hello进程挂起。

4.crtl+z:

图6.6.7

这个操作可以向进程发送一个sigtstp信号,让进程暂时挂起,输入ps可以看到hello进程还没有被终止,并且其pid为5842。此时我们可以用kill命令让该进程被终止。再次使用ps,可以看到hello进程已经被终止。
也可以使用fg让后台挂起程序继续运行:

图6.6.8

可以看到在挂起前,hello进程输出了四次,fg后输出了六次,说明确实是继续运行的。
5. jobs:

图6.6.9

可以查看当前的关键命令。












6.pstree

图6.6.8

可以用进程树的方法把各个进程用树状图的方式连接起来。

6.7本章小结
本章中主要介绍了hello在执行过程中的异常信号处理,以及shell的处理流程,同时通过堆异常控制流的介绍,我们能够更加深入了解了系统对于异常的处理机制。而hello也完成它的作用即hello的进程被执行。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。在hello.o当中地址就以逻辑地址的形式展现,也就是其相对偏移地址。
线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
虚拟地址:程序访问存储器所使用的逻辑地址称为虚拟地址。是hello里的虚拟内存地址。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。计算机系统的主存被组织成一个由M 个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址。是hello里虚拟内存地址对应的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
1.基本原理:
在段式存储管理中,将程序的地址空间划分为若干个段(segment),这样每个进程有一个二维的地址空间。在段式存储管理系统中,为每个段分配一个连续的分区,而进程中的各个段可以不连续地存放在内存的不同分区中。程序加载时,操作系统为所有段分配其所需内存,这些段不必连续,物理内存的管理采用动态分区的管理方法。
在为某个段分配物理内存时,可以采用首先适配法、下次适配法、最佳适配法等方法。
在回收某个段所占用的空间时,要注意将收回的空间与其相邻的空间合并。
段式存储管理也需要硬件支持,实现逻辑地址到物理地址的映射。
程序通过分段划分为多个模块,如代码段、数据段、共享段:

  1. 可以分别编写和编译
  2. 可以针对不同类型的段采取不同的保护
  3. 可以按段为单位来进行共享,包括通过动态链接进行代码共享
    这样做的优点是:可以分别编写和编译源程序的一个文件,并且可以针对不同类型的段采取不同的保护,也可以按段为单位来进行共享。
    总的来说,段式存储管理的优点是:没有内碎片,外碎片可以通过内存紧缩来消除;便于实现内存共享。缺点与页式存储管理的缺点相同,进程必须全部装入内存。

2.段式管理的数据结构:
为了实现段式管理,操作系统需要如下的数据结构来实现进程的地址空间到物理内存空间的映射,并跟踪物理内存的使用情况,以便在装入新的段的时候,合理地分配内存空间。
·进程段表:描述组成进程地址空间的各段,可以是指向系统段表中表项的索引。每段有段基址(baseaddress),即段内地址。
在系统中,为每个进程建立一张段映射表,以三十二位系统为例:015位为段内地址,1631位为段号。
·系统段表:系统所有占用段(已经分配的段)。
·空闲段表:内存中所有空闲段,可以结合到系统段表中。

3.段式管理的地址变换:

图7.2.1

7.3 Hello的线性地址到物理地址的变换-页式管理
1.基本原理
将程序的逻辑地址空间划分为固定大小的页(page),而物理内存划分为同样大小的页框(page frame)。程序加载时,可将任意一页放入内存中任意一个页框,这些页框不必连续,从而实现了离散分配。该方法需要CPU的硬件支持,来实现逻辑地址和物理地址之间的映射。在页式存储管理方式中地址结构由两部构成,前一部分是虚拟页号(VPN),后一部分为虚拟页偏移量(VPO):

图7.3.1

分页模式的基本思想:当任务运行时,当前活跃的执行代码保留在内存中,而程序中当前未使用的部分,将继续保存在磁盘上。当CPU需要执行的当前代码存储在磁盘上时,产生一个缺页错误,引起所需页面的换进(从磁盘载入内存)。

2.页表的数据结构
页表将虚拟内存映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。页表是一个页表条目(PTE)的数组。虚拟地址空间的每个页在页表中一个固定偏移量处都有一个PTE。假设每个PTE是由一个有效位和一个n位地址字段组成的。有效位表明了该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置,这个物理页中缓存了该虚拟页。如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配。否则,这个地址就指向该虚拟页在磁盘上的起始位置。

图7.3.2

  1. 页式管理地址变换
    MMU利用VPN来选择适当的PTE,将列表条目中PPN和虚拟地址中的VPO串联起来,就得到相应的物理地址。

图7.3.3
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是翻译后被缓冲器,是一个小的,虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。通常TLB有高度的相联度。用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。如果TLB有T=2t个组,那么TLB索引由VPN的t个最低位组成为TLB标记由VPN中剩下的位组成
地址翻译过程:
1)CPU产生一个虚拟地址
2)MMU从TLB中取出相应PTE
3)MMU将这个虚拟地址翻译成一个物理地址,并把它发送到主存或高速缓存
4)主存或高速缓存将所请求得数据字返回给CPU
而当TLB不命中时MMU从L1缓存中去除相应PTE,新取出的PTE存放在TLB中,可能会覆盖一个已经存在的条目。
而系统实际上采用多级页表的方式进行翻译。

图7.4.1

图7.4.2
7.5 三级Cache支持下的物理内存访问
以Core i7内存系统为例。
处理器封装包括4个核,一个大的所有核共享的L3高速缓存;一个DD3的内存控制器。灭个和包括一个层次结构的TLB、一个层次结构的数据和指令高速缓存,以及一组快速的点到点链路。

图7.5.1
得到物理地址之后,先将物理地址拆分成CT(标记)+CI(索引)+CO(偏移量),然后在一级cache内部找,如果未能寻找到标记位为有效的字节(miss)的话就去二级和三级cache中寻找对应的字节,找到之后返回结果。
7.6 hello进程fork时的内存映射
Linux通过将虚拟内存区域与磁盘上的对象关联起来以初始化这个虚拟内存区域的内容. 这个过程称为内存映射。
虚拟内存和内存映射解释了fork函数如何为每个新进程提供私有的虚拟地址空间。
为新进程创建虚拟内存:

  1. 创建当前进程的mm_struct、vm_area_struct和页表的原样副本。
  2. 两个进程中的每个页面都标记为只读
  3. 两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)
    在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存, 随后的写操作通过写时复制机制创建新页面.
    7.7 hello进程execve时的内存映射
    加载并运行hello需要以下几个步骤:
  4. 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
  5. 创建新的区域结构:
    1.代码和初始化数据映射到.text和.data区(目标文件提供)
    2.bss和栈映射到匿名文件
    3.映射共享区域:如果hello程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
  6. 设置PC,指向代码区域的入口点

图7.7.1
7.8 缺页故障与缺页中断处理
当指令引用一个虚拟地址,在MMU中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就会发生故障。
而发生故障时触发故障处理程序,系统将控制权交给缺页处理程序,它选择一个牺牲页面,将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新执行引起缺页的指令。
例如CPU引用了VP3,VP3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE3,从有效位推断出VP3未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4。如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4的页表条目,之后VP4就不再缓存在主存中。
缺页之前:

图7.8.1

接下来,内核从磁盘复制VP3到内存中的PP3,更新PTE3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在VP3已经缓存在主存中了,则命中。

图7.8.2
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
而动态内存管理主要有四种方法:
1) 隐式空闲链表:通过头部中的大小字段隐含地连接空闲块。
2) 显示空闲链表:在空闲块中使用指针连接空闲块
3) 分离地空闲链表:每个大小类地空闲链表包含大小相等的块。
4) 按照尺寸排序的块:可以使用平衡树(例如红黑树),在每个空闲块中有指针,尺寸作为键。
这里我们介绍的是隐式空闲链表。

  1. 结构
    对于每个块我们都需要知道块的大小和分配状态。由于每个块是八个字节对齐的,故低阶的三位总是是0,我们可以将这些0改为已分配/未分配的状态的标志,而在读段的大小时,可以使用掩码将其屏蔽掉。每个块的结构如下:

图7.9.1

使用方法示例:

图7.9.2
而对于显式链表有:

图7.9.3

每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。使用双向链表使首次适配的时间减少到空闲块数量的线性时间。
空闲链表中块的排序策略:一种是用后进先出的顺序维护链表,将新释放的块放置在链表的开始处,另一种方法是按照地址顺序来维护链表,链表中每个块的地址都小于它后继的地址。

  1. 分配策略:
    隐式链表的分配策略有:
    1) 首次适配: 从头开始搜索空闲链表,选择第一个合适的空闲块;
    特点:倾向在靠近链表起始处留下小空闲块的;“碎片”搜索时间与总块数(包括已分配和空闲块)是线性关系;
    缺点:增加对较大块的搜索时间
    2) 下一次适配: 从链表中上一次查询结束的地方开始
    比首次适应更快: 避免重复扫描那些无用块
    一些研究表明,下一次适配的内存利用率要比首次适配低得多
    3) 最佳适配:查询链表,选择一个最好的空闲块
    优点:适配,剩余空闲空间最少;保证碎片最小,提高内存利用率。
    缺点:运行速度通常会慢于首次适配

  2. 分配空闲块:分割
    申请空间比空闲块小:把空闲块分割成两部分

图7.9.4

4. 释放块:
	清除“已分配位”标志。

图7.9.5

	缺点:有可能产生假碎片。

5. 合并相邻的空闲块
	简单策略:和下一块合并
	缺点:可能产生大量碎片。

图7.9.6

双向合并:

利用边界标记:在空闲块的”底部”标记”大小/已分配”,每次合并前检查上一块的尾部,若为空闲块,则可以和上一块进行合并。

图7.9.6
合并情况分为以下四种:

图7.9.7
7.10本章小结
本章介绍了储存器的地址空间,讲述了虚拟地址、物理地址、线性地址、逻辑地址的概念,还有进程fork和execve时的内存映射的内容。描述了系统如何应对那些缺页异常,最后描述了malloc的内存分配管理机制,这些问题都是hello在运行过程中需要关注到的问题。只有有一个良好的内存管理和分配策略,程序才能正常快速运行。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix I/O接口

  1. 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
  2. Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述
    符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
    ——改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为K 。
    ——读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k>=m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号” 。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。
    ——关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

Unix I/O函数
1.open
进程通过调用open函数打开一个已存在的文件或者创建一个新文件

int open(char *filename, int flags, mode_t mode);

open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件:

  1. O_RDONLY 只读

  2. O_WRONLY 只写

  3. O_RDWR 可读可写
    flags也可以是一个或者更多位掩码的或,为写提供一些额外的指示。mode参数制定了新文件的访问权限

  4. close
    进程通过调用你close函数关闭一个打开的文件

int close(int fd)

关闭一个已关闭的描述符时会出错。close函数的返回值是close的操作结果,若成功返回1反之返回-1

  1. read/write
    进程通过调用read,wirte来执行输入和输出

int read(int fd, void *buf, size_t n)

若成功返回读的字节数 若EOF则为0若出错则为-1

int write(int fd, void *buf, size_t n)

若成功返回写的字节数,若出错则为-1。其中buf代表内存位置,n表示要读/写的字节数(从buf开始读/写)
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是一个字符指针,arg表示函数的第二个参数。
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的作用是格式化。它接受确定输出格式的格式字符串fnt。用格式字符串对个数变化的参数进行格式化,产生格式化输出,并返回要打印的字符串的长度。
write的代码:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL

先给寄存器传了几个参数,然后通过系统调用sys_call:
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(存储每一个点的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;
}
getchar函数调用read函数,将整个缓冲区都读到buf里,并将缓冲区的长度赋值给n。返回时返回buf的第一个元素,除非n<0。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Unix I/O接口以及函数,同时通过对printf函数以及getchar函数的分析,对Unix I/O进行了更加深刻的介绍,也更加明显得展现了Unix I/O的作用。
(第8章1分)

结论

  1. 程序员通过IDE或者其他的文本编辑器创建了hello.c
  2. 文件hello.c被预处理器经过预处理调用外部库展开为hello.i
  3. 文件hello.i被编译器编译成hello.s,变为一个汇编语言程序
  4. 汇编器将hello.s汇编成机器码,把这些机器指令打包为hello.o,这个文件叫做可重定位的目标文件。
  5. 链接器将hello.o与动态链接库等链接成为可执行文件hello。
  6. 用户在shell中输入./hello 1180300918 邹建
  7. 在进程中shell调用fork函数创建子进程。
  8. 在这个子进程中,shell用函数execve来调用启动器加载器。
  9. 程序hello在一个时间片里执行自己的控制流,同时访问内存以及申请动态内存,并且可以接受ctrl+z,ctrl+c的信号
  10. hello调用sleep,getchar,exit等系统函数后进程结束,被父进程shell回收,内核删除为子进程创建的数据结构。

至此hello的一生结束了。

可以说程序员熟悉一个新的环境,最先尝试的一定是不同意义下的hello语句,而计算机系统也不例外。只是没想到这次的hello如此凶险(苦笑)。通过亲手操作并且一步一步阅读、理解每一个阶段,原本十分简单的hello语句变得复杂的不行,我也第一次成功地调戏,或者阻碍了程序的运行,也是第一次如此详尽地了解了hello的一生,也最终帮助hello程序突破层层阻碍,从物理内存一步一步到达了我的输出设备屏幕,和我的学号还有名字相见,可以说是意料之中的别致惊喜了。

就像前面所说的,尽管hello程序非常简单,但是为了让它实现运行,系统的每个主要组成部分都需要协调工作。通过这次大作业,我了解了在系统上执行hello程序时,系统发生了什么以及为什么会这样。我也逐步了解到了在程序运行的每一步中,计算机各个部分的功能,同时了解了在面对各种异常时的处理机制,这将有助于我对计算机系统的深入理解。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值