HIT-CSAPP程序人生大作业

第1章 概述

1.1 Hello简介

1.1.1 P2P: From Program to Process

P2P,即从程序到进程。用户利用高级语言C语言编写hello.c源程序,经过cpp预处理形成hello.i,再经过ccl编译形成汇编语言程序hello.s,然后经过as转换为机器语言指令,形成可重定位目标程序hello.o,最后通过ld与库函数链接并符号解析与重定位,形成可执行目标文件hello。而后可执行文件hello通过shell加载,fork产生子进程,经过以上步骤hello程序(program)变成了hello进程(process)。

1.1.2 020: From Zero-0 to Zero -0

020,即从运行到结束。初始时内存中没有hello文件相关的内容,通过fork产生hello子进程后,通过execve进行加载,先删除当前虚拟地址已存在的数据结构,为hello的代码、数据、bss等创建区域,然后映射共享区域,设置程序计数器,进入main函数,CPU分配时间片执行逻辑控制流。执行过程中,虚拟内存为进程提供独立的空间;存储结构层层递进,让数据从磁盘传输到CPU中;TLB、分级页表等也为数据的高效访问提供保障;I/O设备通过描述符与接口实现了hello的输入输出。多方面合作配合之下,hello完成执行。然后,shell回收hello进程,删除hello的所有痕迹,释放运行中占用的内存空间。至此,hello从运行到结束,完成020过程。

1.2 环境与工具

1.2.1 硬件环境

X64 CPU;2GHz;2G RAM;256GHD Disk

1.2.2 软件环境

Windows 10 64位;Vmware-workstation-16;Ubuntu 20.04 LTS 64位

1.2.3 开发与调试工具

Codeblocks 64位;vi/vim/gedit+gcc

1.3 中间结果

表1.3-1 中间结果

文件名

功能

hello.c

源代码

hello.i

预处理后的文本文件

hello.s

编译后的汇编文件

hello.o

汇编后的可重定位目标文件

hello

链接后的可执行文件

hello.elf

用readelf读取hello.o的ELF格式信息

hello.asm

反汇编hello.o的反汇编文件

hello2.elf

由hello可执行文件生成的.elf文件

hello2.asm

反汇编hello可执行文件得到的反汇编文件

1.4 本章小结

本章主要介绍了hello的P2P和020过程,从总体上简要阐述了hello的一生,给出了论文研究时的环境与工具以及中间生成的文件信息。

第2章 预处理

2.1 预处理的概念与作用

2.1.1概念

预处理器(cpp) 根据以字符#开头的命令,修改原始的C 程序,形成完整的文件。C语言的预处理主要有三个方面的内容: 1.宏定义;(#define 标识符 文本) 2.文件包含;(#include "文件名") 3.条件编译。(#ifdef,#else,#endif)

2.1.2作用

  1. 宏替换。例如:#define a 10,则在预处理时将所有的a变为10。
  2. 文件包含。预处理器读取头文件中的内容,并插入到程序文本中。例如:#include <stdio.h>则将stdio.h的代码扎入到hello中。
  3. 删除注释。

条件编译。根据某个条件判断进行静态编译。主要有#ifdef, #else, #elif, #endif, #ifndef等条件语句。

2.2在Ubuntu下预处理的命令

命令:gcc -E hello.c -o hello.i

图2-1.预处理命令

2.3 Hello的预处理结果解析

预处理后的源文件从23行扩展到了3060行,main函数在第3047行,预处理器将头文件中的内容引入hello.i,将需要用到的库函数等加入到了文本中,让程序能够继续被编译器编译。

  1. 包含文件信息

图2.3-1 hello.i中部分文件信息

  1. 类型定义信息

图2.3-2 hello.i中部分类型定义信息

  1. 函数声明信息

图2.3-3 hello.i中部分函数声明信息

  1. 源码部分

图2.3-4 hello.i中源码部分

2.4 本章小结

在本章中,我们对hello.c进行了预处理,生成hello.i,预处理器会进行宏展开、头文件展开、条件编译等处理,并删除注释,对函数源码并不做过多修改,hello.i文件可用于下一步的处理。

第3章 编译

3.1 编译的概念与作用

3.1.1 编译的概念

编译是指预处理后,编译器(ccl)将预处理文件hello.i翻译成汇编语言文件hello.s。 

3.1.2 编译的作用

  1. 语法检查:检查代码是否存在语法错误,如果有错误的话就会报错。
  2. 生成汇编代码:将程序翻译成汇编语言,从而在下一阶段可以让汇编器翻译成机器语言指令。
  3. 代码优化:编译器会对程序进行优化,生成效率更高的目标代码。

3.2 在Ubuntu下编译的命令

编译命令:gcc -S hello.i -o hello.s

图3.2-1 编译命令

3.3 Hello的编译结果解析

3.3.1 汇编代码展示

  1.   .file "hello.c"
  2. .text
  3. .section .rodata
  4. .align 8
  5. .LC0:
  6. .string "\323\303\267\250: Hello 2022112322 \300\356\352\277\342\371 15645376975 0! "
  7. .LC1:
  8. .string "Hello %s %s %s\n"
  9. .text
  10. .globl main
  11. .type main, @function
  12. main:
  13. .LFB6:
  14. .cfi_startproc
  15. endbr64
  16. pushq %rbp
  17. .cfi_def_cfa_offset 16
  18. .cfi_offset 6, -16
  19. movq %rsp, %rbp
  20. .cfi_def_cfa_register 6
  21. subq $32, %rsp
  22. movl %edi, -20(%rbp)
  23. movq %rsi, -32(%rbp)
  24. cmpl $5, -20(%rbp)
  25. je .L2
  26. leaq .LC0(%rip), %rdi
  27. call puts@PLT
  28. movl $1, %edi
  29. call exit@PLT
  30. .L2:
  31. movl $0, -4(%rbp)
  32. jmp .L3
  33. .L4:
  34. movq -32(%rbp), %rax
  35. addq $24, %rax
  36. movq (%rax), %rcx
  37. movq -32(%rbp), %rax
  38. addq $16, %rax
  39. movq (%rax), %rdx
  40. movq -32(%rbp), %rax
  41. addq $8, %rax
  42. movq (%rax), %rax
  43. movq %rax, %rsi
  44. leaq .LC1(%rip), %rdi
  45. movl $0, %eax
  46. call printf@PLT
  47. movq -32(%rbp), %rax
  48. addq $32, %rax
  49. movq (%rax), %rax
  50. movq %rax, %rdi
  51. call atoi@PLT
  52. movl %eax, %edi
  53. call sleep@PLT
  54. addl $1, -4(%rbp)
  55. .L3:
  56. cmpl $9, -4(%rbp)
  57. jle .L4
  58. call getchar@PLT
  59. movl $0, %eax
  60. leave
  61. .cfi_def_cfa 7, 8
  62. ret
  63. .cfi_endproc
  64. .LFE6:
  65. .size main, .-main
  66. .ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0"
  67. .section .note.GNU-stack,"",@progbits
  68. .section .note.gnu.property,"a"
  69. .align 8
  70. .long  1f - 0f
  71. .long  4f - 1f
  72. .long  5
  73. 0:
  74. .string  "GNU"
  75. 1:
  76. .align 8
  77. .long  0xc0000002
  78. .long  3f - 2f
  79. 2:
  80. .long  0x3
  81. 3:
  82. .align 8
  83. 4:

3.3.2 汇编文本结构分析

表3.2.2-1 hello.s文件结构

内容

含义

.file

源文件

.text

代码段

.global

全局变量

.data

存放已经初始化的全局和静态C 变量

.section  .rodata

存放只读变量

.align

对齐方式

.type

表示是函数类型/对象类型

.size

表示大小

.long  .string

表示是long类型/string类型

3.3.3 常量

编译时对常量进行编码,并将其存储在只读代码区的 .rodata节,在程序运行时会直接通过寻址找到常量。

例如将hello.c中“Usage: Hello 学号 姓名 手机号 秒数!”编译为汇编代码第6行:

  1. .string "\323\303\267\250: Hello 2022112322 \300\356\352\277\342\371 15645376975 0! "

3.3.4 变量

不同类型的变量在不同位置定义,初始化的全局变量和静态变量定义在只读代码区的.bss节,已初始化的全局和静态变量定义在只读代码区的.data节,局部变量在堆上进行定义和释放。

例如,局部变量int i被保存在栈上,通过机器指令对其赋值

  1. movl $0, -4(%rbp)

3.3.5 赋值操作

对局部变量进行赋值操作,使用MOV指令,根据不同的数据大小选择不同指令movb、movw、movl、movq等。具体见3.3.4例。

3.3.5 算术运算

表3.3.5-1 算数指令

指令

效果

leaq s,d

d=&s

inc d

d+=1

dec d

d-=1

neg d

d=-d

add s,d

d=d+s

sub s,d

d=d-s

imulq s

r[%rdx]:r[%rax]=s*r[%rax]

mulq s

r[%rdx]:r[%rax]=s*r[%rax]

idivq s

r[%rdx]=r[%rdx]:r[%rax] mod s

 r[%rax]=r[%rdx]:r[%rax] div s

divq s

r[%rdx]=r[%rdx]:r[%rax] mod s

 r[%rax]=r[%rdx]:r[%rax] div s

在hello.s中,例如,实现i++的操作:

  1. addl $1, -4(%rbp)

开辟栈以及回收栈:

  1. subq $32, %rsp

3.3.6 比较和跳转操作

通过COM指令进行比较,计算两个值相减大小,根据结果设置条件码,根据条件码来判断跳转值,也可通过跳转指令J判断有无符号。

例如,检查argc是否不等于4。在hello.s中,使用cmpl $4,-20(%rbp),比较 argc与4的大小并设置条件码,为下一步je利用条件码进行跳转作准备。

  1. cmpl $5, -20(%rbp)
  2. je .L2

表3.3.6-1 跳转指令

指令

条件

jmp

直接跳转

je

相等

Jne

不等于

Js

小于

Jns

小于等于

Jg

大于

Jge

大于等于

Ja

大于(无符号)

Jae

大于等于(无符号)

jbe

小于等于(无符号)

3.3.7 数组/指针操作

对数组的索引相当于在第一个元素地址的基础上通过加索引值乘以数据大小来实现。

例如,在hello.c中,存在char *argv[],根据图3-3可知,根据argv首地址获得argv[1]和argv[2]需要通过加减操作:

  1. movq -32(%rbp), %rax
  2. addq $16, %rax
  3. movq (%rax), %rdx
  4. movq -32(%rbp), %rax
  5. addq $8, %rax
  6. movq (%rax), %rax
  7. movq %rax, %rsi

3.3.8 函数操作

hello.c中包括main函数,printf函数,sleep函数,getchar函数,exit函数。

首先,内核shell获取命令行参数和环境变量地址,执行main函数,在main中需要调用其它函数,在main中为被调用函数分配栈空间。调用函数需要借助栈,先将返回地址压入栈中,并将PC设为被调用函数的起始地址,然后调用。返回时,先从栈中弹出返回地址,再PC设置为该返回地址。return正常返回后,leave恢复栈空间。

在hello.s中调用函数有:

call puts@PLT

call exit@PLT

call printf@PLT

call sleep@PLT

call getchar@PLT

3.4 本章小结

本章主要探讨编译器将经过预处理阶段后的C程序hello.i翻译成汇编语言程序的处理过程,包括对数据、算术操作、关系操作、控制转移、数组操作、函数操作的处理。编译器也会在处理过程中对程序进行一些优化,最终的结果被保存在hello.s文件中,能够在下一阶段让汇编器翻译机器语言指令。

第4章 汇编

4.1 汇编的概念与作用

4.1.1 汇编的概念

汇编是指汇编器(as)将hello.s翻译成机器语言指令的过程,把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件hello.o中。

4.1.2 汇编的作用

将汇编指令转换成机器可以直接读取分析的机器指令,生成hello.o文件,用于后续的链接。

4.2 在Ubuntu下汇编的命令

汇编命令:gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o

图4.2-1 汇编命令

4.3 可重定位目标elf格式

在shell中输入readelf -a hello.o > hello.elf指令获得hello.o文件的 ELF 格式:

图4.3-1 生成elf格式文件

4.3.1 ELF头

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

图4.3.1-1 ELF头

4.3.2 节头

节头记录了各节名称及大小、类型及全体大小、地址及旗标、连接、信息和偏移量及对齐信息。

图4.3.2-1节头

4.3.3 重定位节

当链接器把这个目标文件和其他文件组合时,需要修改表中的这些位置。一般,调用外部函数或者引用全局变量的指令都需要修改。

其中包括R_X86_64_PC32( PC相对地址的引用)和R_X86_64_32(绝对地址的引用)。

图4.3.3-1重定位节

4.3.4 符号表

符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。

图4.3.4-1符号表

4.4 Hello.o的结果解析

使用objdump -d -r hello.o > hello.asm分析hello.o的反汇编,并与第3章的 hello.s文件进行对照分析。

图4.4-1生成反汇编文件

4.4.1 反汇编代码

图4.4-2反汇编代码

4.4.2 与汇编代码比较

将hello.asm和hello.s进行比较,大部分相同,主要有一下几个方面不同:

  1. 包含内容:

hello.s中包含.type .size .align以及.rodata只读数据段等信息,而hello.asm中只有函数的相关内容。

2.分支转移:

在hello.s中,跳转指令的目标地址直接记为段名称,如.L1,.L2等。而在反汇编得到的hello.asm中,跳转的目标为具体的地址。 

3.函数调用:

在hello.s文件中,call之后直接跟着函数名称,而在反汇编得到的hello.asm中,call 的目标地址是当前指令的下一条指令。

4. 全局变量访问:

在hello.s 文件中,使用段名称+%rip访问 rodata(printf 中的字符串),而在反汇编得到的hello.asm中,使用 0+%rip进行访问。

4.4.3 机器语言的构成

机器语言是机器能直接识别的程序语言或指令代码,无需经过翻译,每一操作码在计算机内部都有相应的电路来完成它,或指不经翻译即可为机器直接理解和接受的程序语言或指令代码。机器语言使用绝对地址和绝对操作码。不同的计算机都有各自的机器语言,即指令系统。从使用的角度看,机器语言是最低级的语言。

4.4.4 机器语言与汇编语言的映射关系

汇编语言和机器语言一般是一一对应的,汇编语言是机器语言的符号表示方式。而不同类型的CPU 有不同的机器指令系统,也就有不同的汇编语言,所以,汇编语言程序与机器有着密切的关系。所以,除了同系列、不同型号CPU 之间的汇编语言程序有一定程度的可移植性之外,其它不同类型(如:小型机和微机等)CPU 之间的汇编语言程序是无法移植的,也就是说,汇编语言程序的通用性和可移植性要比高级语言程序低。

4.5 本章小结

本章讨论了汇编阶段将汇编代码hello.s翻译成机器语言指令hello,o的过程,等待下一步链接器的处理。同时,比较了汇编代码与反汇编代码之间的不同之处。

5链接

5.1 链接的概念与作用

5.1.1 链接的概念

链接是指链接器(ld)将各种代码和数据片断收集并组合成一个单一可执行目标文件的过程。

5.1.2 链接的作用

使得分离编译成为可能,我们可以独立的修改和编译模块,当我们改变这些模块的其中一个时,只需简单的重新编译它,并重新链接应用,而不必重新编译其他文件。

5.2 在Ubuntu下链接的命令

链接命令:ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o hello.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o -o hello

图5.2-1 链接命令

5.3 可执行目标文件hello的格式

在Shell中输入命令 readelf -a hello > hello2.elf 生成 hello 程序的 ELF 格式文件,保存为hello2.elf:

图5.3-1 生成hello.o的elf格式

5.3.1 ELF头

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

图5.3.1-1 ELF头

5.3.2 节头

节头记录了各节名称及大小、类型及全体大小、地址及旗标、连接、信息和偏移量及对齐信息。

图5.3.2-1节头

5.3.3 程序头

一个结构数组,描述了系统准备程序执行所需的段或其他信息。

图5.3.3-1程序头

5.3.4 Dynamic section(动态section)

图5.3.4-1 Dynamic section

5.3.5 符号表

符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。

图5.3.5-1符号表

5.4 hello的虚拟地址空间

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

根据计算机系统的特性,程序被载入至地址0x400000~0x401000中。在该地址范围内,每个节的地址都与前一节中节对应的 Address 相同。通过ELF可知,程序从0x00400000到0x00400fff,在0x400fff之后存放的是.dynamic到.shstrtab节的内容。

图5.4-1用edb查看虚拟地址空间

5.5 链接的重定位过程分析

在Shell中使用命令objdump -d -r hello > hello2.asm生成反汇编文件hello2.asm,与第四章中生成的hello.o.asm文件进行比较:

图5.5-1生成反汇编文件

5.5.1 重定位概念

链接器在完成符号解析以后,就把代码中的每个符号引用和一个符号定义关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。

然后就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时的地址。在 hello 到 hello.o 中,首先是重定位节和符号定义,链接器将所有输入到 hello 中相同类型的节合并为同一类型的新的聚合节。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。然后是重定位节中的符号引用,链接器会修改 hello 中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址。

5.5.2 hello与hello.o的不同

1.链接后函数数量增加,hello中增加了外部函数。

2.函数调用指令call的参数发生变化,hello中调用为call+函数名,hello.o中为call+相对偏移地址。

3.跳转指令参数发生变化。

4.hello.o中是相对偏移地址,hello为虚拟内存地址。

5.6 hello的执行流程

hello在执行的过程中要执行载入、执行和退出三个过程,列出其调用与跳转的各个子程序名或程序地址。

表5.6-1 程序名称与程序地址

程序名称

程序地址

<_start>

4010f0

<__libc_csu_init>

4011c0

<_init>

401000

<main>

401125

<.plt>

401020

<puts@plt>

401090

<printf@plt>

4010a0

<atoi@plt>

4010c0

<exit@plt>

4010d0

<sleep@plt>

4010e0

<getchar@plt>

4010b0

<__libc_csu_fini>

401230

<_fini>

401238

5.7 Hello的动态链接分析

延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:

PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。

GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。

图5.7-1 全局偏移表的位置

图5.7-2 调用 dl_init 之前的全局偏移表

图5.7-3 调用 dl_init 之后的全局偏移表

由上图对比可知,调用dl_init函数后,发生动态链接,GOT条目改变。

5.8 本章小结

本章介绍了链接的概念和功能,分析可执行文件hello的ELF格式及其虚拟地址空间,并对重定位、动态链接进行深入的分析。

(第5章1分)


6hello进程管理

6.1 进程的概念与作用

6.1.1 进程的概念

进程的经典定义就是一个执行中的程序的实例。进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

6.1.2 进程的作用

每次用户通过向 shell 输入一个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。

6.2 简述壳Shell-bash的作用与处理流程

6.2.1 Shell作用

读取输入并解析命令行;替换特别字符,比如通配符和历史命令符;设置管道、重定向和后台处理;处理信号;程式执行的相关设置。

1. 可交互,和非交互的使用shell。在交互式模式,shell从键盘接收输入;在非交互式模式,shell从文件中获取输入。

2. shell中可以同步和异步的执行命令。在同步模式,shell要等命令执行完,才能接收下面的输入。在异步模式,命令运行的同时,shell就可接收其它的输入。重定向功能,可以更细致的控制命令的输入输出。另外,shell允许设置命令的运行环境。

3. shell提供了少量的内置命令,以便自身功能更加完备和高效。

4. shell除了执行命令,还提供了变量,流程控制,引用和函数等,类似高级语言一样,能编写功能丰富的程序。

5. shell强大的的交互性除了可编程,还体现在作业控制,命令行编辑,历史命令,和别名等方面。

6.2.2 Shell 处理流程

命令行是一串 ASCII 字符由空格分隔。字符串的第一个单词是一个可执行程序,或者是 shell 的内置命令。命令行的其余部分是命令的参数。

如果第一个单词是内置命令,shell 会立即在当前进程中执行。否则,shell 会新建一个子进程,然后再子进程中执行程序。新建的子进程又叫做作业。通常,作业可以由 Unix 管道连接的多个子进程组成。

如果命令行以&符号结尾,那么作业将在后台运行,这意味着在打印提示符并等待下一个命令之前,shell 不会等待作业终止。否则,作业在前台运行,这意味着 shell 在作业终止前不会执行下一条命令行。 因此,在任何时候,最多可以在一个作业中运行在前台。 但是,任意数量的作业可以在后台运行。例如,键入命令行:sh> jobs,会让 shell 运行内置命令 jobs。键入命令行 sh> /bin/ls -l -d 会导致 shell 在前台运行 ls 程序。根据约定,shell会执行程序的 main 函数 int main(int argc, char *argv[]),argc 和 argv 会接收到下面的值:

argc == 3,

argv[0] == ‘‘/bin/ls’’,

argv[1]== ‘‘-l’’,

argv[2]== ‘‘-d’’.

下面以&结尾的命令行会在后台执行 ls 程序:

sh> /bin/ls -l -d &

Unix shell 支持作业控制的概念,允许用户在前台和后台之间来回移动作业,并更改进程的状态(运行,停止或终止)。在作业运行时,键入 ctrl-c会将 SIGINT 信号传递到前台作业中的每个进程。SIGINT 的默认动作是终止进程。类似地,键入 ctrl-z 会导致 SIGTSTP 信号传递给所有前台进程。SIGTSTP 的默认操作是停止进程,直到它被 SIGCONT 信号唤醒为止。Unixshell 还提供支持作业控制的各种内置命令。例如:

jobs:列出运行和停止的后台作业。

bg <job>:将停止的后台作业更改为正在运行的后台作业。

fg <job>:将停止或运行的后台作业更改为在前台运行。

kill <job>:终止作业。

6.3 Hello的fork进程创建过程

父进程通过调用fork函数创建一个新的、处于运行状态的子进程。

函数原型:int fork(void);

调用fork函数后,子进程返回0,父进程返回子进程的PID;新创建的子进程几乎但不完全与父进程相同:子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本;子进程获得与父进程任何打开文件描述符相同的副本;但是子进程有不同于父进程的PID。fork函数:被调用一次,却返回两次!

Fork具体处理hello文件过程:

首先,带参执行当前目录下的可执行文件hello,父进程会通过fork函数创建一个新的运行的子进程hello。子进程获取了与父进程的上下文,包括栈、通用寄存器、程序计数器,环境变量和打开的文件相同的一份副本。子进程与父进程的最大区别是有着跟父进程不一样的PID,子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由init进程回收子进程。  

图6.3-1程序执行过程

6.4 Hello的execve过程

调用函数fork创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新程序hello。execve 函数从不返回,它将删除该进程的代码和地址空间内的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序。将私有的区域映射进来,例如打开的文件,代码、数据段,然后将公共的区域映射进来。后面加载器跳转到程序的入口点,即设置PC指向_start 地址。_start函数最终调用hello中的 main 函数,这样,便完成了在子进程中的加载。

6.5 Hello的进程执行

6.5.1 进程时间片

一个进程执行它的控制流的一部分的每一时间段叫做时间片。

6.5.2 上下文信息

上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。

6.5.3 用户模式和内核模式

处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

6.5.4 执行过程

进程调度指在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个过程称为调度。内存收到中断信号之后,将当前进程加入等待序列,进行上下文切换将当前的进程控制权交给其他进程,当再次收到中断信号时将hello从等待队列加入运行队列。

6.6 hello的异常与信号处理

异常可以分为四类:中断(interrupt),陷阱(trap),故障(fault)和终止(abort)。

中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。在当前指令完成执行后,处理器注意到中断引脚的电压变高,就从系统总线读取异常号,然后调用适当的中断处理程序。当处理程序返回后,它就将控制返回给下一条指令。

陷阱是有意的异常,是执行一条指令的结果。应用程序执行一次系统调用,然后把控制传递给处理程序,陷阱处理程序运行后,返回到syscall之后的指令。

故障由错误情况引起,故障发生时处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序。

终止是不可恢复的致命错误造成的结果。终止处理程序从不将控制返回给应用程序,处理程序将控制返回给一个abort例程,该例程会终止这个应用程序。

以下给出程序运行过程中按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令的运行结果:

图6.6-1 执行Ctrl-c

图6.6-2 执行Ctrl-z以及ps、jobs

图6.6-4 执行pstree

图6.6-5 执行kill

图6.6-6 执行fg返回前台

6.7本章小结

本章主要讨论了进程和shell的概念与作用,进程的创建和执行过程,以及对异常和信号的处理。

(第6章1分)


7hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:逻辑地址是指由程序产生的与段相关的偏移地址部分,即hello.o 里面的相对偏移地址。

线性地址:地址空间是一个非负整数地址的有序集合,如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间,即hello里面的虚拟内存地址。

虚拟地址:CPU 通过生成一个虚拟地址,即hello里面的虚拟内存地址。

物理地址:计算机系统的主存被组织成一个由M 个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址。就是hello在运行时虚拟内存地址对应的物理地址。

7.2 Intel逻辑地址到线性地址的变换-段式管理

一个逻辑地址由两部分组成,段标识符和段内偏移量。段标识符由 16 位字段组成,前 13 位为索引号。索引号是段描述符的索引,很多个描述符,组成了一个数组,叫做段描述表,可以通过段描述标识符的前 13 位,在这个表中找到一个具体的段描述符,这个描述符就描述了一个段,每个段描述符由八个字节组成。base 字段,描述了段开始的线性地址,一些全局的段描述符,放在全局段描述符表中,一些局部的则对应放在局部段描述符表中。由 T1 字段决定使用哪个。

图7-1.段选择符

以下是具体的转化步骤:

1. 给定一个完整的逻辑地址,[段选择符:段内偏移地址]

2. 看段选择符 T1,知道要转换的是 GDT 中的段还是 LDT 中的段,通过寄存器得到地址和大小。

3. 取段选择符中的 13 位,再数组中查找对应的段描述符,得到 BASE,就是基地址。

4. 线性地址等于基地址加地址偏移量。

对于偏移量,基址寄存器还是变址寄存器有不同的计算方法,后者需要经过乘比例因子等处理。

7.3 Hello的线性地址到物理地址的变换-页式管理

ntel处理器从线性地址到物理地址的变换通过页式管理实现。

图7-2.二级页表结构

具体步骤为:

1、从CR3中取出进程的页目录的地址,取出其前20位,这是页目录的基地址;       

2、根据取出来的页目录的基地址以及线性地址的前十位,进行组合得到线性地址的前十位的索引对应的项在页目录中地址,根据该地址可以取到该地址上的值,该值就是二级页表项的基址;

3、根据第二步取到的页表项的基址,取其前20位,将线性地址的10-19位左移2位,按照和第2步相同的方式进行组合就可以得到线性地址对应的物理页框在内存中的地址在二级页表中的地址的起始地址,根据该地址向后读四个字节就得到了线性地址对应的物理页框在内存中的地址在二级页表中的地址,然后取该地址上的值就得到线性地址对应的物理页框在内存中的基地址;       

4、根据第3步取到的基地址,取其前20位得到物理页框在内存中的基址,再根据线性地址最后的12位的偏移量得到具体的物理地址,取该地址上的值就是最后要得到值;

7.4 TLB与四级页表支持下的VA到PA的变换

针对Intel Core i7 CPU研究VA到PA的变换。

Intel Core i7 CPU的基本参数如下:

  1. 虚拟地址空间48位(n=48)
  2. 物理地址空间52位(m=52)
  3. TLB四路十六组相连
  4. L1,L2,L3块大小为64字节
  5. L1,L2八路组相连
  6. L3十六路组相连
  7. 页表大小4KB(P=4x1024=2^12),四级页表,页表条目(PTE)大小8字节

由上述信息可以得知,VPO与PPO有p=12位,故VPN为36位,PPN为40位。单个页表大小4KB,PTE大小8字节,则单个页表有512个页表条目,需要9位二进制进行索引,而四级页表则需要36位二进制进行索引,对应着36位的VPN。TLB有16组,故TLBI有t=4位,TLBT有36-4=32位。

图 50 TLB与四级页表支持下的VA到PA的变换

如图所示, CPU产生虚拟地址VA,并将其传送至MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)在TLB中进行匹配,若命中,则得到PPN(40bit)与VPO(12bit)组合成物理地址PA(52bit)。若TLB没有命中,则MMU向页表中查询,由CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,则执行下一步确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA,并向TLB中添加条目。多级页表的工作原理展示如下:

s

若查询PTE的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。

7.5 三级Cache支持下的物理内存访问

图7-5. 三级Cache支持下的物理内存访问

CPU 发出一个虚拟地址在 TLB 里搜索,如果命中,直接发送到 L1cache 里;如果没有命中,就现在加载到表里之后再发送过去,到了 L1 中,寻找物理地址又要检测是否命中,如果没有命中,就向 L2/L3 中查找。这就用到了 CPU高速缓存。

在层与层之间读取数据时,首先在高速缓存中查找所需字w的副本。如果命中,立即返回字w给CPU。如果不命中,从存储器层次结构中较低层次中取出包含字w的块,将这个块存储到某个高速缓存行中(可能会驱逐一个有效的行),然后返回字w。

读取数据分为三种:

1)直接映射高速缓存

直接映射高速缓存每个组只有一行,当CPU执行一条读内存字w的指令,它会向L1高速缓存请求这个字。如果L1高速缓存中有w的一个缓存副本,那么就会得到L1高速缓存命中,高速缓存会很快抽取出w,并将它返回给CPU。否则就是缓存不命中,当L1高速缓存向主存请求包含w的块的一个副本时,CPU必须等待。当被请求块最终从内存到达时,L1高速缓存将这个块存放在它的一个高速缓存行里,从被存储的块中抽取出字w,然后将它返回给CPU。确定是否命中然后抽取的过程分为三步:(1)组选择;(2)行匹配;(3)字选择。

组选择即从w的地址中间抽取出s个索引位,将其解释为一个对应组号的无符号整数,从而找到对应的组;行匹配即对组内的唯一一行进行判断,当有效位为1且标记位与从地址中抽取出的标记位相同则成功匹配,否则就得到不命中;而字选择即在行匹配的基础上通过地址的后几位得到块偏移,从而在高速缓存块中索引到数据。

图7-6.组选择

图7-7.行匹配和字选择

2)组相联高速缓存

组相联高速缓存每个组内可以多于一个缓存行,总体逻辑类似于直接映射高速缓存,不同之处在于行匹配时每组有更多的行可以尝试匹配,遍历每一行。如果不命中,有空行时也就是冷不命中则直接存储在空行;如果没有空行也就是冲突不命中,则替换已有行,通常有LFU(最不常使用)、LRU(最近最少使用)两者替换策略。

3)全相联高速缓存

全相联高速缓存只有一个组,且这个组包含所有的高速缓存行(即E =C/B)。对于全相联高速缓存,因为只有一个组,组选择变的十分简单。地址中不存在索引位,地址只被划分为一个标记位和一个块偏移。行匹配和字选择同组相联高速缓存。

写入数据时,有两种方法来更新w在层次结构中紧接着低一层中的副本,分别是直写和写回:

1)直写

立即将w的高速缓存块写回到紧挨着的低一层中。虽然简单,但是每次写都会引起总线流量。其处理不命中的方法是非写分配,即避开高速缓存,直接将这个字写到低一层去。

(2)写回

尽可能地推迟更新,只有当替换算法要驱逐这个更新过的块时,才把它写到紧接着的低一层中。虽然能显著地减少总线流量,但是增加了复杂性,必须为每个高速缓存行增加一个额外的修改位,表明是否被修改过。写回处理不命中的方法是写分配,加载相应低一层中的块到高速缓存中,然后更新这个高速缓存块,试图利用写的空间局部性,但会导致每次不命中都会有一个块从低一层传到高速缓存。

通过这样的Cache读写机制,实现了从CPU寄存器到L1高速缓存,再到L2高速缓存,再到L3高速缓存,再到物理内存的访问,极大地提高了速度和效率。

7.6 hello进程fork时的内存映射

图7-8.fork进程

当fork函数被当前进程hello调用时,内核为新进程hello创建各种数据结构,并分配给它一个唯一的PID。为了给这个新的hello创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当着两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行包含可执行目标文件hello中的程序,加载、运行 hello 需要以下步骤:

1. 删除已存在的用户区域。删除 shell 虚拟地址的用户部分中的已存在的区域结构。

2. 映射私有区域。为 hello 的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text 和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中。栈和堆区域也是请求二进制零的,初始长度为零。

3. 映射共享区域。如果 hello 程序与共享对象(或目标)链接,比如标准 C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

4. 设置程序计数器(PC) 。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

经过这个内存映射的过程,在下一次调度hello进程时,就能够从hello的入口点开始执行了。

7.8 缺页故障与缺页中断处理

图7-9.缺页

DRAM 缓存不命中称为缺页(page fault). 图7-9为缺页之前我们的示例页表的状态。CPU 引用了 VP 3 中的一个字,VP 3 并未缓存在 DRAM 中。地址翻译硬件从内存中读取 PTE 3, 从有效位推断出 VP 3 未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在 PP3 中的 VP4 。如果 VP4 已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改 VP4 的页表条目,反映出 VP 4 不再缓存在主存中这一事实。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap) 。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址) 。对于每个进程,内核维护着一个变量brk, 它指向堆的顶部。

分配器将堆视为一组不同大小的块(block) 的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

基本方法:这里指的基本方法应该是在合并块的时候使用到的方法,有最佳适配和第二次适配还有首次适配方法,首次适配就是指的是第一次遇到的就直接适配分配,第二次顾名思义就是第二次适配上的,最佳适配就是搜索完以后最佳的方案,当然这种的会在搜索速度上大有降低。

策略:这里的策略指的就是显式的链表的方式分配还是隐式的标签引脚的方式分配还是分离适配,带边界标签的隐式空闲链表分配器允许在常数时间内进行对前面块的合并。这种思想是在每个块的结尾处添加一个脚部,其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。显式空间链表就是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继指针,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。为了分配一个块,必须确定请求的大小类,并且对适当的空闲链表做首次适配,查找一个合适的块。如果找到了一个,那么就(可选地)分割它,并将剩余的部分插入到适当的空闲链表中。如果找不到合适的块,那么就搜索下一个更大的大小类的空闲链表。如此重复,直到找到一个合适的块。如果空闲链表中没有合适的块,那么就向操作系统请求额外的堆内存,从这个新的堆内存中分配出一个块,将剩余部分放置在适当的大小类中。要释放一个块,我们执行合并,并将结果放置到相应的空闲链表中。

图7-10.使用边界标记的堆块

7.10本章小结

本章结合书上第六章和第九章的知识(主要是第九章),介绍了存储器的地址空间,讲述了虚拟地址、物理地址、线性地址、逻辑地址的概念,还有进程fork和execve时的内存映射,并详细描述了系统如何应对缺页异常,最后描述了malloc的内存分配管理机制。

(第7章 2分)


8hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有的输入和输出都能以一种统一且一致的方式来执行:打开文件、改变当前的文件位置、读写文件、关闭文件。

8.2 简述Unix IO接口及其函数

  1. Unix I/O接口:
  1. 打开文件

一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。对于Shell创建的每个进程,其都有三个打开的文件:标准输入,标准输出,标准错误。

  1. 改变当前的文件位置

对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。

  1. 读写文件

一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

  1. 关闭文件

内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

  1. Unix I/O函数:
  1. int open(char* filename,int flags,mode_t mode)

进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。

  1. int close(fd)

fd是需要关闭的文件的描述符,close返回操作结果。

  1. ssize_t read(int fd,void *buf,size_t n)

read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。

  1. ssize_t wirte(int fd,const void *buf,size_t n)

write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。

8.3 printf的实现分析

查看windows系统下的printf函数体:

图 53 printf的函数体

形参列表中的…是可变形参的一种写法,当传递参数的个数不确定时,用这种方式来表示。

va_list的定义:typedef char *va_list,说明它是一个字符指针,其中 (char*)(&fmt) + 4) 即arg表示的是...中的第一个参数。

再进一步查看windows系统下的vsprintf函数体:

图 54 vsprintf的函数体

则知道vsprintf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。

在printf中调用系统函数write(buf,i)将长度为i的buf输出。write函数如下:

printf函数的功能为接受一个格式化命令,并按指定的匹配的参数格式化输出,故i = vsprintf(buf, fmt, arg)是得到打印出来的字符串长度,其后的write(buf, i)是将buf中的i个元素写到终端。

因此,vsprintf的作用为接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,进而产生格式化输出。

再进一步对write进行追踪:

图 55 write的情况

这里给几个寄存器传递了参数,然后以一个int INT_VECTOR_SYS_CALL结束。INT_VECTOR_SYS_CALL代表通过系统调用syscall,查看syscall的实现:

图 56 syscall的情况

syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码,符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 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函数可知:

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。getchar的大概思想是读取字符串的第一个字符之后再进行返回操作。

8.5本章小结

本章介绍了Unix I/O,通过LinuxI/O设备管理方法以及Unix I/O接口及函数了解系统级I/O的底层实现机制,并通过对printf和getchar函数的底层解析加深对Unix I/O以及异常中断等的了解。

(第8章1分)

结论

  1. 预处理

将hello.c中include的所有外部的头文件头文件内容直接插入程序文本中,完成字符串的替换,方便后续处理;

  1. 编译

通过词法分析和语法分析,将合法指令翻译成等价汇编代码。通过编译过程,编译器将hello.i 翻译成汇编语言文件 hello.s;

  1. 汇编

将hello.s汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在hello.o 目标文件中;

  1. 链接

通过链接器,将hello的程序编码与动态链接库等收集整理成为一个单一文件,生成完全链接的可执行的目标文件hello;

  1. 加载运行

打开Shell,在其中键入 ./hello 1190200208 李旻翀,终端为其fork新建进程,并通过execve把代码和数据加载入虚拟内存空间,程序开始执行;

  1. 执行指令

在该进程被调度时,CPU为hello其分配时间片,在一个时间片中,hello享有CPU全部资源,PC寄存器一步一步地更新,CPU不断地取指,顺序执行自己的控制逻辑流;

  1. 访存

内存管理单元MMU将逻辑地址,一步步映射成物理地址,进而通过三级高速缓存系统访问物理内存/磁盘中的数据;

  1. 动态申请内存

printf 会调用malloc 向动态内存分配器申请堆中的内存;

  1. 信号处理

进程时刻等待着信号,如果运行途中键入ctr-c ctr-z 则调用shell 的信号处理函数分别进行停止、挂起等操作,对于其他信号也有相应的操作;

  1. 终止并被回收

Shell父进程等待并回收hello子进程,内核删除为hello进程创建的所有数据结构。

这一生,和人类类似,从出生到一点点成长,一步步长大,并且是一个老实听话的人,如果中途遇到其他事情,便会立刻的执行。等它忙碌了一辈子后,最终还是要入土为安,被回收走,不留一点痕迹。

感悟:

总体回顾hello的一生,觉得计算机系统也是有生命的,这一生也充满了很多故事。这一生我们理解起来很艰难,但是我们可以通过对 hello一生的追溯来学习计算机对一个程序的处理过程,有助于我们以后写出更好的代码,并且在对 hello 一步步操作的学习中我们加深了对计算机系统的理解。

当做这个大作业时,彻底将看似孤立的每一章连了起来,知道了它们的前因后果,并且觉得这门课还是挺有意思的。其实,如果这个大作业能在开学初的时候布置下来,随着章节的学习一步步地做,可能会消化理解得更好一些。

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


附件

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

文件名

功能

hello.i

预处理后得到的文本文件

hello.s

编译后得到的汇编语言文件

hello.o

汇编后得到的可重定位目标文件

hello.elf

用readelf读取hello.o得到的ELF格式信息

hello.asm

反汇编hello.o得到的反汇编文件

hello2.elf

由hello可执行文件生成的.elf文件

hello2.asm

反汇编hello可执行文件得到的反汇编文件

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


参考文献

为完成本次大作业你翻阅的书籍与网站等

[1] Randal E.Bryant, David O'Hallaron. 深入理解计算机系统[M]. 机械工业出版社.2018.4 

[2] Pianistx.printf 函数实现的深入剖析[EB/OL].2013[2021-6-9].

https://www.cnblogs.com/pianist/p/3315801.html

[3] 梦想之家xiao_chen.ELF文件头更详细结构[EB/OL].2017[2021-6-10].

https://blog.csdn.net/qq_32014215/article/details/76618649.

[4] Florian.printf背后的故事[EB/OL].2014[2021-6-10].

https://www.cnblogs.com/fanzhidongyzby/p/3519838.html.

(参考文献0分,缺失 -1分)

  • 18
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
《计算机系统:程序员的视角(第三版,全球版,2015年7月)》是作者Randal E. Bryant和David R. O'Hallaron的经典教材,介绍了计算机系统的实现和设计方面的基本概念。这本书着重于C语言和x86-64汇编语言上,这两种语言是现代计算机编程中最常用的语言之一。本书在计算机科学和工程方面的许多领域都提供了一个坚实的基础,包括操作系统、编译器、计算机体系结构和网络,可以让读者理解计算机系统的每个层面。 本书分为三部分:程序、机器级表示和系统级表示,每个部分都逐渐深入,涵盖了计算机系统的基础知识,如处理器、存储器、缓存、虚拟内存和文件系统等方面。读者可以通过学习本书中的例子和练习来学习使用工具,例如调试器、汇编器和高级语言编译器等,在实践中掌握系统级别编程的基础知识。书中还提供了许多实用技巧和编程技巧,用于更高效和更精确地编写C程序和汇编程序。 这本书的一个显著优点是它对操作系统的原理提供了深入的覆盖范围,以及在系统级别上构建应用程序的详细说明。这使得本书非常适合计算机科学和工程领域的学生,以及想要深入了解计算机系统的程序员和系统或网络管理员。通过学习本书,读者可以充分理解系统级别编程和操作系统设计的基本原理,从而提高基础编程技能并为将来的学习和职业发展打下坚实的基础。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值