计算机系统

大作业

题 目 程序人生-Hello’sP 2 P

专 业 计算学部

学 号 xxxxxxxxxx

班 级 1903008

学 生 xx

指 导 教 师 吴锐

计算机科学与技术学院

2021 年 5 月

摘 要

本文阐述了hello.c从预处理、编译、汇编到链接再到运行中间经过的全过

程,在Linux系统下结合edb、objdump等工具进行分析。运行阶段,以hello为

例,分析了fork、execve等函数以及程序是如何访问内存等内容。结合课本和网

络资料,将理论知识和hello实例结合起来更加形象深入的学习计算机系统。

关键词:汇编;编译;链接;虚拟内存;动态申请内存

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

目 录

第 1 章 概述 …- 4 -

1. 1 HELLO简介…- 4 -

1. 2 环境与工具…- 4 -

1. 3 中间结果…- 4 -

1. 4 本章小结…- 5 -

第 2 章 预处理 …- 6 -

2. 1 预处理的概念与作用…- 6 -

2. 2 在UBUNTU下预处理的命令…- 6 -

2. 3 HELLO的预处理结果解析…- 7 -

2. 4 本章小结…- 9 -

第 3 章 编译 …- 10 -

3. 1 编译的概念与作用…- 10 -

3. 2 在UBUNTU下编译的命令…- 10 -

3. 3 HELLO的编译结果解析…- 11 -

3. 4 本章小结…- 22 -

第 4 章 汇编 …- 23 -

4. 1 汇编的概念与作用…- 23 -

4. 2 在UBUNTU下汇编的命令…- 23 -

4. 3 可重定位目标ELF格式…- 23 -

4. 4 HELLO.O的结果解析…- 27 -

4. 5 本章小结…- 29 -

第 5 章 链接 …- 30 -

5. 1 链接的概念与作用…- 30 -

5. 2 在UBUNTU下链接的命令…- 30 -

5. 3 可执行目标文件HELLO的格式…- 30 -

5. 4 HELLO的虚拟地址空间…- 32 -

5. 5 链接的重定位过程分析…- 34 -

5. 6 HELLO的执行流程…- 35 -

5. 7 HELLO的动态链接分析…- 36 -

5. 8 本章小结…- 36 -

第 6 章 HELLO 进程管理 …- 37 -

6. 1 进程的概念与作用…- 37 -

  1. (^2) 简述壳SHELL-BASH的作用与处理流程…- 37 -
  2. 3 HELLO的FORK进程创建过程…- 37 -
  3. 4 HELLO的EXECVE过程…- 38 -
  4. 5 HELLO的进程执行…- 38 -
  5. 6 HELLO的异常与信号处理…- 39 -
  6. 7 本章小结…- 42 -
    7HELLO 的存储管理 …- 43 -
  7. 1 HELLO的存储器地址空间…- 43 -
  8. 2 INTEL逻辑地址到线性地址的变换-段式管理…- 43 -
  9. 3 HELLO的线性地址到物理地址的变换-页式管理…- 44 -
  10. 4 TLB与四级页表支持下的VA到PA的变换…- 44 -
  11. 5 三级CACHE支持下的物理内存访问…- 46 -
  12. 6 HELLO进程FORK时的内存映射…- 46 -
  13. 7 HELLO进程EXECVE时的内存映射…- 47 -
  14. 8 缺页故障与缺页中断处理…- 47 -
  15. 9 动态存储分配管理…- 48 -
  16. (^10) 本章小结…- 49 -
    8HELLOIO 管理 …- 50 -
  17. 1 LINUX的IO设备管理方法…- 50 -
  18. 2 简述UNIXIO接口及其函数…- 50 -
  19. 3 PRINTF的实现分析…- 51 -
  20. 4 GETCHAR的实现分析…- 53 -
  21. 5 本章小结…- 53 -
    结论 …- 53 -
    附件 …- 55 -
    参考文献 …-^56 -

第 1 章 概述

  1. 1 Hello简介

1 )P 2 P简述:

用户通过编辑器编写代码,完成 hello.c源程序文本的编写;在提前搭建好的

Ubuntu系统下,调用预处理器(cpp)得到预处理后的文本程序hello.i;接着调用

编译器(ccl)得到汇编文本程序hello.s;然后汇编器(as)将hello.s翻译成机器

语言指令,把这些指令打包成可重定位目标程序的格式,得到可重定位目标文件

hello.o;最后通过链接器(ld)得到可执行目标文件hello。用户在shell输入./hello

执行此程序,shell调用fork函数为其产生子进程,hello便最终成为了一个进程。

2 )O 2 O简述:

OS的进程管理调用fork函数,产生子进程,调用execve函数,并进行虚拟内存

映射,并为运行的hello分配时间片来执行取指译码流水线等操作;当CPU引用

一个被映射的虚拟页时,可执行目标文件hello中的代码和数据从磁盘复制到物理

内存,然后跳转到程序入口点_start 函数的地址,最终调用执行hello中的 main 函

数,OS的储存管理以及MMU解决VA到PA的转换,cache、TLB、页表等加速

访问过程;程序结束时,shell回收hello进程,内核从系统中清除它的痕迹。

1. 2 环境与工具

硬件工具:

处理器:Intel®Core™i 5 - 9300 HCPU@ 2. 40 GHz 2. 40 GHz
已安装的内存(RAM): 8. 00 GB( 7. 81 GB 可用)

软件工具:
Windows 10 家庭中文版;VirtualBox 6. 1 ;Ubuntu 20. 04

开发者与调试工具:
gcc,gdb,edb, vim,readelf,objdump等

1. 3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

生成的中间结果文件名 文件的作用

hello.i hello.c预处理后得到的文件

hello.s hello.i编译后得到的文件

hello.o hello.s汇编后的可重定位目标文件

hello-elf.txt hello.o的elf格式文件

hello hello.o经过链接后的可执行目标文件

hello-out-elf.txt hello的elf格式文件

hello-asm.txt hello的反汇编代码文件

hello-o-asm.txt hello.o的反汇编代码文件

1. 4 本章小结

本章简要介绍了hello.c的P 2 P与O 2 O,然后列举了本篇文章用到的环境与工具以
及生成的中间文件(包括它们的作用)。
(第 1 章 0. 5 分)

第 2 章 预处理

2. 1 预处理的概念与作用

预处理的概念:

预处理是在程序编译之前进行的处理,可放在程序中任何位置。预处理是 C

语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,

系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入

对源程序的编译。

ISOC和ISOC++都规定程序由源代码被翻译分为若干有序的阶段,通常前几

个阶段由预处理器实现。预处理中会展开以#起始的行,试图解释为预处理指令,

其中ISOC/C++要求支持的包括#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、

#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、

#pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来

使源代码在不同的执行环境中被方便的修改或者编译。

预处理的作用:

主要和三个部分有关:宏定义,文件包含,条件编译

(^1) 、宏定义。预处理程序中的#define 标识符文本,预处理工作也叫做宏展开:将
宏名替换为文本(这个文本可以是字符串、可以是代码等)。
2 、文件包含。预处理程序中的#include,将头文件的内容插入到该命令所在的位
置,从而把头文件和当前源文件连接成一个源文件。
3 、条件编译相关。根据#if以及#endif和#ifdef以及#ifndef来判断执行编译的条件。
2. 2 在Ubuntu下预处理的命令
命令 1 :gcc-Ehello.c-ohello.i
执行后效果如下图所示:

图 2. 2. 1 命令 1 执行效果

命令 2 :cpphello.c>hello.i

执行后效果如图所示:

图 2. 2. 2 命令 2 执行效果

  1. 3 Hello的预处理结果解析

hello.i文件的部分截图:

图 2. 3. 1 hello.i文件部分截图

仔细观察hello.i文本文件,可以发现由于经过了预处理,原本 23 行的hello.c文件

现在变成了 3060 行,这主要是因为预处理过程实现了头文件的展开,宏替换和去

注释并作条件编译。

利用ctrl+f查询,可以发现stdio.h文件的展开开始于 13 行,结束于 728 行,unistd.h

文件的展开开始于 730 行,结束于 1966 行,stdlib.h文件的展开开始于 1969 行,

结束于 3041 行。可以根据给出的路径找到这三个文件的实际位置,如下图所示:

图 2. 3. 2 stdio.h、unistd.h、stdlib.h文件所在地
  1. 4 本章小结

本章主要介绍了预处理的概念和功能以及Ubuntu下预处理的两条指令,将hello.c

预处理得到了hello.i文本,并分析其中的过程。因此,预处理过程主要是负责:

头文件的展开、宏替换、去掉注释、条件编译。

(第 2 章 0. 5 分)

第 3 章 编译

3. 1 编译的概念与作用

编译的概念:

编译就是将源语言经过词法分析、语法分析、语义分析以及经过一系列优化

后生成汇编代码的过程,它是将高级语言程序转化为机器可直接识别处理执行的

的机器码的中间步骤。

编译的作用:

它包括以下几个部分。

1 、词法分析。对输入的字符串进行分析和分割,形成所使用的源程序语言所允许

的记号,同时标注不规范记号,产生错误提示信息。

2 、语法分析。分析词法分析得到的记号序列,并按一定规则识别并生成中间表示

形式,以及符号表。同时将不符合语法规则的记号识别出其位置并产生错误提示

语句。

3 、语义分析。即静态语法检查,分析语法分析过程中产生的中间表示形式和符号

表,以检查源程序的语义是否与源语言的静态语义属性相符合。

(^4) 、代码优化。将中间表示形式进行分析并转换为功能等价但是运行时间更短或占
用资源更少的等价中间代码。
3. 2 在Ubuntu下编译的命令
编译的命令:gcc-Shello.i-ohello.s
执行结果如下图所示:

图 3. 2 hello.i编译后结果
  1. 3 Hello的编译结果解析

先展示hello.s的截图:

图 3. 3. 1 hello.s的截图上
图 3. 3. 2 hello.s的截图下
3. 3. 1 head.s开头部分

hello.s的开头部分如下:

图 3. 3. 1 hello.s开头部分

.file表示源文件,这里是hello.c

.text 声明下面是代码段

.section.rodata 声明下面是rodata节

.align 声明指令、数据存放地址的对齐方式,这里是 8

.long.string 声明long、string类型

.globl 声明全局变量 main

.type 声明是函数类型还是对象类型,这里是main@function

3. 3. 2 各种数据类型

hello.s用到的C语言数据类型有:整数、字符串、数组。

1 )整数

对于intargc:

argc是main函数的第一个参数,代表了argv指针数组的数组元素个数。从hello.s

里可以找到argc被存入了- 20 (%rbp)地址下,如下图所示:

图 3. 3. 2. 1 存储argc参数

对于inti:

C语言中的局部变量在运行时被保存在栈或者是寄存器里。对于这里的inti,编译

器将它存储在了栈空间中,具体是- 4 (%rbp)下,如图所示:

图 3. 3. 2. 2 存储int变量

对于常数立即数:

源程序中出现的常数如 0 , 1 , 2 , 10 等是直接在汇编代码中出现的,汇编代码是

允许立即数以$常数形式存在的。

图 3. 3. 2. 3 常数立即数

2 )字符串

程序中的字符串如图所示:

图 3. 3. 2. 4 字符串

"用法:Hello 学号 姓名 秒数!\n"是第一个printf函数传入的输出格式化参数,在

hello.s中表示如下:

"\ 347 \ 224 \ 250 \ 346 \ 263 \ 225 : Hello \ 345 \ 255 \ 246 \ 345 \ 217 \ 267

\ 345 \ 247 \ 223 \ 345 \ 220 \ 215 \ 347 \ 247 \ 222 \ 346 \ 225 \ 260 \ 357 \ 274 \ 201 "

汉字采用UTF- 8 格式,一个汉字在UTF- 8 编码中用三个字节来表示,用\区分每个

字节。

“Hello%s%s\n””,是第二个printf函数传入的输出格式化参数,也是只读的。

3 )数组

对于数组char*argv[]:

它是main函数中的第二个形式参数,来源于执行时输入的参数。argv 每个指针元

素 char*大小为 8 个字节,argv指针指向已经分配好的、一片存放着字符指针的连

续空间,起始地址为 argv。在main函数内部,对argv[^1 ],argv[^2 ]的访问来源于对

数组首地址argv进行加法计算得到相应的地址。

下图是引用argv数组内容的部分截图:

图 3. 3. 2. 5 argv数组的使用

3. 3. 3 程序中的赋值

程序中的赋值一般情况下都用mov指令实现,数据长度不同,所用到的指令也就

不同。举个例子,为inti赋值i= 0 ,汇编语句如下:

可以看到由于i是int类型的数据,故要用的是movl指令传送 4 字节大小的值。

图 3. 3. 3 程序中变量i的赋值

3. 3. 4 程序中的类型转换

常见的几种转换方式及其后果:

1 、从int转换为float时,不会发生溢出,但可能有数据被舍入

2 、从int或 float转换为double时,因为double的有效位数更多,故能保留精确

3 、从double转换为float和int时,可能发生溢出,此外,由于有效位数变少,故

可能被舍入

4 、从float 或double转换为int时,因为int没有小数部分,所以数据可能会向 0

方向被截断

程序中调用了atoi函数,把字符串转换成整型数。其作用是将用户在命令行上输

入的argv[ 3 ]给转换成int类型值传递给sleep函数,即将秒数传递给sleep函数。它

的头文件包含在#include<stdlib.h>中。

3. 3. 5 程序中的算数操作

下面列举书上常见的整数算术操作:

图 3. 3. 5. 1 常见整数算术操作

下面举点hello.s中的例子:

1 )每次循环之后对i执行+ 1 操作

图 3. 3. 5. 2 i++操作

2 )对栈顶指针的操作

图 3. 3. 5. 3 栈顶指针操作

3. 3. 6 程序中的比较操作

常见的指令有:

图 3. 3. 6. 1 cmp指令
图 3. 3. 6. 2 jump指令

hello.s中用到了对jle和je的条件码判断,如下图所示:

判断argc是否等于 4

图 3. 3. 6. 3 判断argc是否等于 4

判断i是否小于 8

图 3. 3. 6. 4 判断i是否小于 8

3. 3. 7 程序中的控制转移

对于if判断)

举例如下:

首先cmpl比较argv和 4 ,并设置条件码,使用 je 判断 ZF 标志位,如果为ZF
为 1 ,说明argv等于 4 ,直接跳转到.L 2 ,否则顺序执行下一条语句。

图 3. 3. 7. 1 if判断

对于for循环)

举例如下:

首先对i赋初值为 0 ,然后用cmpl进行比较,如果 i<= 7 ,则跳入.L 4 ,执行循环体

内部的操作,否则循环结束,执行for循环之后的代码。

图 3. 3. 7. 2 for循环

3. 3. 8 函数操作

函数提供了一种封装代码的方式,用一组指定的参数和一个可选的返回值实现了

某种功能。

源代码中涉及的函数有:

1 )main函数

传递控制:call 指令将下一条指令的地址压栈,然后跳转到 main 函数;传递数

据:向main函数传递参数argc和argv,分别使用%rdi 和%rsi存储,将%eax作为

返回值,设置为^0 。

2 )printf 函数

传递数据:将%rdi设置为字符串的首地址

图 3. 3. 8. 1 printf函数调用 1
图 3. 3. 8. 2 printf函数调用 2

控制传递:第一次callputs;第二次callprintf。

3 )atoi函数

传递参数

图 3. 3. 8. 3 atoi函数调用

控制转移:callatoi

4 )sleep函数

传递参数:将%edi设置为atoi(argv[ 3 ])

控制传递:callsleep

5 )getchar 函数:

控制传递:callgetchar

图 3. 3. 8. 4 getchar函数调用

6 )exit 函数:

传递数据:将%edi 设置为 1 。

控制传递:callexit

图 3. 3. 8. 5 exit函数调用

3. 3. 9 最终结果

经过编译后,hello.s还是文本文件的形式,CPU无法直接执行,编译只是将高级

语言程序转化为机器可直接识别处理执行的的机器码的中间步骤。

3. 4 本章小结

本章详细介绍了编译的概念与作用以及Ubuntu下编译的指令,然后从数据类型、

赋值操作、类型转换、算术操作、控制转移和函数操作等 9 个方面细致分析了hello.s

文件的内容。

(第 3 章 2 分)

第 4 章 汇编

4. 1 汇编的概念与作用

概念:

驱动程序运行汇编器as,将汇编语言翻译成机器语言指令,并把这些指令打包成
可重定位目标程序的格式,并将结果保存在二进制目标文件的过程称为汇编。

作用:
汇编就是将高级语言转化为机器可直接识别执行的代码文件的过程,汇编器将.s
文件翻译成机器语言指令,并将这些指令打包成可重定位目标程序的格式,将结
果保存在.o 二进制目标文件中。

  1. 2 在Ubuntu下汇编的命令

gcc-ohello.s-ohello.o
执行结果如图所示:

图 4. 2 执行完gcc-ohello.s-o hello.o之后的结果
  1. 3 可重定位目标elf格式
4. 3. 1 读取可重定位目标文件

输入命令readelf-ahello.o>hello-elf.txt将elf可重定位目标文件输出定向到文本文
件hello.elf中,截图如下:

图 4. 3. 1 hello-elf.txt

4. 3. 2 典型的ELF可重定位目标文件的表格

ELF头

.text
.rodata
.data
.bss
.symtab
.rel.text
.rel.data
.debug
.line
.strtab
节头部表

4. 3. 3 列出ELF文件各个节的内容

ELF头:

以 16 字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。

ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF

头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中

条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中

每个节都有一个固定大小的条目。截图如下:

图 4. 3. 3. 1 ELF头

下面是具体分析:

Magic: 7 f 454 c 46020101000000000000000000
Magic是魔数,确定文件的类型或格式,加载或读取文件时,可用魔数确认文件类
型是否正确

类别:ELF 64
代表ELF 64 格式

数据: 2 补码,小端序 (littleendian)
二进制补码类型,小端

Version: 1 (current)
版本信息Version

OS/ABI
UNIX-SystemV

ABI 版本: 0
ABI版本

类型:REL(可重定位文件)
目标文件格式 可重定位文件

系统架构:AdvancedMicroDevicesX 86 - 64
AdvancedMicroDevicesX 86 - 64 的机器

版本: 0 x 1
版本

入口点地址:^0 x^0
程序执行的入口地址

程序头起点: 0 (bytesintofile)
段头部表的开始

Startofsectionheaders: 1240 (bytesintofile)
节头部表的开始

标志: 0 x 0
标志

Sizeofthisheader: 64 (bytes)
头大小

Sizeofprogramheaders: 0 (bytes)
段头部表大小

Numberofprogramheaders: 0
段头部表数量

Sizeofsectionheaders: 64 (bytes)
节头部表大小

Numberofsectionheaders: 14
节头部表数量

Sectionheaderstringtableindex: 13
字符串表在节头部表中的索引

.text:已编译程序的机器代码。

.rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表。

.data:已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不
出现在.data节中,也不出现在.bss中。

.bss:未初始化的全局和静态C变量,以及所有被初始化为 0 的全局或静态C变量。

.symtab:一个符号表,他存放在程序中定义和引用的函数和全局变量的信息

.rel.text:一个.text节中位置的列表,链接时修改

.rel.data:被模块引用或定义的所有全局变量的重定位信息

.debug:条目是局部变量、类型定义、全局变量及C源文件

.line:C源程序中行号和.text节机器指令的映射

.strtab:.symtab和.debug中符号表及节头部中节的名字

节头部表:节头表包括节名称,节的类型,节的属性(读写权限),节在ELF文
件中所占的长度以及节的对齐方式和偏移量。

图 4. 3. 3. 2 节头部表

  1. 4 Hello.o的结果解析

命令行输入objdump-d-rhello.o
结果如图所示:

图 4. 4. 1 objdump-d-r hello.o的执行结果
4. 4. 1 分支转移

hello.s文件中分支转移是使用段名称进行跳转的,而hello.o文件中分支转移是通
过地址进行跳转的。

图 4. 4. 1. 1 hello.s中的分支转移
图 4. 4. 1. 2 hello.o中的分支转移

4. 4. 2 函数调用

hello.s文件中,函数调用call后跟的是函数名称;而在hello.o文件中,call后跟的
是下一条指令。而当这些函数都是共享库函数时,地址是不确定的,因此call指
令将相对地址设置为全 0 ,然后在.rela.text节中为其添加重定位条目,等待链接的
进一步确定。

图 4. 4. 2. 1 hello.s调用函数
图 4. 4. 2. 2 hello.o调用函数

4. 4. 3 全局变量

hello.s文件中,全局变量是通过语句:段地址+%rip完成的;对于hello.o来说,则是:
0 +%rip,因为.rodata节中的数据是在运行时确定的,也需要重定位,现在填 0 占位,
并为其在.rela.text节中添加重定位条目。

图 4. 4. 3. 1 hello.s访问全局变量
图 4. 4. 3. 2 hello.o访问全局变量

4. 4. 4 机器语言

机器语言程序是二进制的机器指令序列集合,完全由二进制数表示,由操作码与

操作数组成。机器语言与汇编语言具有一一对应的映射关系,每一行机器代码对

应一行汇编代码。机器代码中的操作数是能被机器直接理解的代码,而汇编代码

是能使人理解CPU操作的低级语言代码。

4. 5 本章小结

本章对汇编结果进行了详细的介绍,介绍了汇编的概念与作用以及在Ubuntu下汇

编的命令。同时本章主要部分在于对可重定位目标elf格式进行了详细的分析。同

时对hello.o文件进行反汇编,与之前生成的hello.s文件进行了对比分析。

(第 4 章 1 分)

第 5 章 链接

5. 1 链接的概念与作用

概念:

链接程序将分别在不同的目标文件中编译或汇编的代码收集到一个可直接执行的

文件中。它还连接目标程序和用于标准库函数的代码,以及连接目标程序和由计

算机的操作系统提供的资源。

作用:

1 )链接可以执行于编译时,也就是源代码被翻译成机器代码时;也可以执行于加

载时,即程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用

程序来执行。

2 )链接使得分离编译成为可能。便于维护管理,可以独立的修改和编译我们需要

修改的小的模块。

  1. 2 在Ubuntu下链接的命令

命令如下:
ld-ohello-dynamic-linker/lib 64 /ld-linux-x 86 - 64 .so. 2 /usr/lib/x 86 _ 64 - linux-gnu/crt 1 .o
/usr/lib/x 86 _ 64 - linux-gnu/crti.o hello.o /usr/lib/x 86 _ 64 - linux-gnu/libc.so
/usr/lib/x 86 _ 64 - linux-gnu/crtn.o

效果如下图所示:

图 5. 2 hello.o链接后得到hello
  1. 3 可执行目标文件hello的格式

获取hello的elf格式文件:readelf-ahello>hello-out-elf.txt

图 5. 3. 1 hello的elf格式文件

各节的基本信息均在节头表中进行了声明,节头表(包括名称,大小,类型,全

体大小,地址,旗标,偏移量,对齐等信息),截图如下:

图 5. 3. 2 节头表

程序头:

图 5. 3. 3 程序头

  1. 4 hello的虚拟地址空间

1 、找到edb所在位置并运行,截图如下:

图 5. 4. 1 找到edb并运行

2 、在edb中打开hello

图 5. 4. 2 在edb中打开hello

3 、打开SymbolViewer窗口
可以看到各节的虚拟地址,截图如下:

图 5. 4. 3 各节的虚拟地址

5. 5 链接的重定位过程分析

1 、获得hello.o和hello的反汇编文件

图 5. 5. 1 获得两个反汇编文件

2 、对比两个反汇编文件

1 )hello-asm.txt比hello-o-asm.txt多了几个节,例如下面的section.init

图 5. 5. 2 区别 1 :多了一些节

2 )hello-asm.txt中用的是虚拟地址,hello-o-asm.txt中用的是相对偏移地址

图 5. 5. 3 区别 2 :虚拟地址和相对偏移地址

3 )hello-asm.txt中增加了许多外部链接的共享库函数。如puts@plt共享库函数,
printf@plt共享库函数以及getchar@plt函数等。

图 5. 5. 4 区别 3 :新增了共享库函数

4 )hello-asm.txt中跳转以及函数调用都用的虚拟地址,例如这里的printf和atoi
函数。

图 5. 5. 5 区别 4 :函数调用、跳转都用了虚拟地址

3 、重定位过程流程

要合并相同的节,确定新节中所有定义符号在虚拟地址空间中的地址,还要对引

用符号进行重定位,修改.text节和.data节中对每个符号的引用。

  1. 6 hello的执行流程

1 、运行edb,打开hello,如图所示:

图 5. 6. 1 运行edb

2 、列出所有过程
函数名称 函数地址
_init 0 x 401000
.plt 0 x 401020
puts@plt 0 x 401090
printf@plt 0 x 4010 a 0
getchar@plt 0 x 4010 b 0
atoi@plt 0 x 4010 c 0

exit@plt 0 x 4010 d 0
sleep@plt 0 x 4010 e 0
_start 0 x 4010 f 0
_dl_relocate_static_pie 0 x 401120
main 0 x 401125
__libc_csu_init 0 x 4011 c 0
__libc_csu_fini 0 x 401230
_fini 0 x 401238

  1. 7 Hello的动态链接分析

在edb中查找.interp段的虚拟地址,这里保存了一个字符串,这个字符串就是动态
库的地址。

图 5. 7. 1 动态库地址

在Datadump中找到此处存放的是/lib 64 /ld-linux-x 86 - 64 .so. 2

图 5. 7. 2 动态链接用到的库

5. 8 本章小结

本章结合实验中的hello可执行程序依次介绍了链接的概念及作用以及Ubuntu下
链接的命令行。然后对hello的elf格式进行了详细的分析,并对比hello的反汇编
文件和hello.o的反汇编文件,详细了解了重定位的过程。最后遍历了整个hello的
执行过程,并对hello进行了动态链接分析。
(第 51 分)

第 6 章 hello进程管理

6. 1 进程的概念与作用

概念:

狭义定义:进程是正在运行的程序的实例

广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活

动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的

分配单元,也是基本的执行单元。

作用:

1 、在现代计算机中,进程为用户提供了以下假象:我们的程序好像是系统中当前

运行的唯一程序 一样,我们的程序好像是独占的使用处理器和内存,处理器好像

是无间断的执行 我们程序中的指令,我们程序中的代码和数据好像是系统内存中

唯一的对象。

2 、每次用户通过向shell 输入一个可执行目标文件的名字,运行程序时,shell 就
会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。
应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码
或其他应用程序。
3 、进程给每个应用提供了两个非常关键的抽象:一是逻辑控制流,二是私有地址
空间。

  1. 2 简述壳Shell-bash的作用与处理流程

Shell-bash的作用:
1 、管理用户与操作系统之间的交互
2 、等待用户输入,向操作系统解释用户的输入
3 、处理各种各样的操作系统的输出结果

Shell-bash的处理流程:
1 、将用户输入的命令行进行解析,分析是否是内置命令;
2 、若是内置命令,直接执行;若不是内置命令,则bash在初始子进程的上下文中
加载和运行它。
3 、本质上就是shell在执行一系列的读和求值的步骤,在这个过程中,他同时可以
接受来自终端的命令输入。

  1. 3 Hello的fork进程创建过程

执行中的进程调用fork()函数,就创建了一个子进程。其函数原型为pid_t

fork(void);对于返回值,若成功调用一次则返回两个值,子进程返回 (^0) ,父进程返
回子进程ID;否则,出错返回- 1 。
首先对于hello进程。我们终端的输入被判断为非内置命令,然后shell试图在硬盘
上查找该命令(即hello可执行程序),并将其调入内存,然后shell将其解释为系
统功能调用并转交给内核执行。
shell执行fork函数,创建一个子进程。这时候我们的hello程序就开始运行了。值
得注意的是,hello子进程是父进程的副本,它将获得父进程数据空间、堆、栈等
资源的副本。但是子进程持有的是上述存储空间的“副本”,这意味着父子进程
间不共享这些存储空间。
同时Linux将复制父进程的地址空间给子进程,因此,hello进程就有了独立的地
址空间。
6. 4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行新程序hello。如果成功,则不返回;
如果错误,则返回- 1 。
在execve加载了hello之后,它调用启动代码。启动代码设置栈,并将控制传递给
hello的主函数。
hello子进程通过execve系统调用启动加载器。
加载器删除子进程所有的虚拟地址段,并创建一组新的代码、数据、堆段。新的
栈和堆段被初始化为 0 。
通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段
被初始化为可执行文件中的内容。
最后加载器跳到_start地址,它最终调用hello的main 函数。除了一些头部信息,
在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚
拟页时才会进行复制,此时,操作系统利用它的页面调度机制自动将页面从磁盘
传送到内存。
6. 5 Hello的进程执行
进程的上下文信息包括通用目的寄存器、浮点寄存器。程序计数器、用户栈、状
态寄存器、内核栈和各种数据结构。
在进行程序调度时,系统保存当前进程的上下文,载入目标进程的上下文,并将
控制传递到目标程序。这些操作都在内核模式下被执行。

图 6. 5 CPU在用户态和内核态间的转变

程序调度频率由CPU的clk时钟周期长度有关,由于CPU时钟周期很短,所以会
给用户造成一种几个程序同时在进行的错觉。

  1. 6 hello的异常与信号处理

异常指的是把控制交给系统内核来响应某些事件(例如处理器状态的变化),其
中内核是操作系统常驻内存的一部分,而这类事件包括除以零、数学运算溢出、
页错误、I/O 请求完成或用户按下了 ctrl+c 等等系统级别的事件。

异常的分类如图所示:

图 6. 6. 1 异常的分类

尝试输入空格,回车或者随便什么数字,如图所示,不影响程序执行。

图 6. 6. 2 空格 回车乱按

按下ctrl+c,程序接收到SIGSTOP信号,程序终止。

图 6. 6. 3 ctrl+c导致程序终止

按下ctrl+z,程序会收到SIGINT信号,进程暂停。

图 6. 6. 4 ctrl+z后进程暂停

用ps查看,仍可看到进程。

图 6. 6. 5 ps查看进程
图 6. 6. 6 pstree查看进程树

输入fg,进程重新在前台工作。

图表 6. 6. 7 fg后进程重新工作

利用kill指令杀死hello进程

图 6. 6. 8 kill指令杀死进程

信号处理:

1 、对于ctrl+c或者ctrl+z:键盘键入后,内核就会发送SIGINT或者SIGSTP。SIGINT
信号默认终止前台job即程序hello,SIGSTP默认挂起前台hello作业。
2 、对于fg信号:内核发送SIGCONT信号,我们刚刚挂起的程序hello重新在前台运
行。
3 、对于kill指令:内核发送SIGKILL信号给指定的pid,杀死该进程。

  1. 7 本章小结

这一章中,我们细致的了解了异常的 4 种类型,包括中断,故障,终止和陷阱,
还有异常控制流的四个基本机制:异常、进程切换、信号和非本地跳转。其中异
常是最底层的,也是后面几种的基础;而信号是进程间最重要,简单却强大的信
使,用来给进程传送信号。
(第 61 分)

第 7 章 hello的存储管理
  1. 1 hello的存储器地址空间

逻辑地址:
逻辑地址是指由程序hello产生的与段相关的偏移地址部分。

线性地址:
线性地址是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑
地址,或者说是段中的偏移地址,它加上相应段的基地址就生成了一个线性地址。

虚拟地址:
我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也
是与实际物理内存容量无关的,是hello中的虚拟地址。

物理地址:
物理地址是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变
换的最终结果地址。如果启用了分页机制,那么hello的线性地址会使用页目录和
页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性
地址就直接成为物理地址了。

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

一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个 16 位长的
字段组成,称为段选择符,其中前 13 位是一个索引号。后面三位包含一些硬件细
节。索引号可以直接理解成数组下标,它对应的“数组”就是段描述符表,段描
述符具体描述了一个段地址,这样,很多段描述符就组成段描述符表。可以通过
段标识符的前 13 位,直接在段描述符表中找到一个具体的段描述符,这个描述符
就描述了一个段。我们只关心Base字段,它描述了一个段的开始位置的线性地址。
逻辑地址转化的过程如下:
首先给定一个完整的逻辑地址[段选择符:段内偏移地址]
1 、看段选择描述符中的T 1 字段是 0 还是 1 ,可以知道当前要转换的是GDT中的
段,还是LDT中的段,再根据指定的相应的寄存器,得到其地址和大小,我们就
有了一个数组了。
2 、拿出段选择符中的前 13 位,可以在这个数组中查找到对应的段描述符,这样
就有了Base,即基地址就知道了。
3 、把基地址Base+Offset,就是要转换的下一个阶段的物理地址。

  1. 3 Hello的线性地址到物理地址的变换-页式管理

下图展示了MMU如何利用页表完成虚拟地址到物理地址的映射:

图 7. 3 使用页表的地址翻译

由图所示,虚拟地址被分为虚拟页号(VPN)与虚拟页偏移量(VPO)。CPU取

出虚拟页号,通过页表基址寄存器(PTBR)来定位页表条目,有效位为 1 时,从

页表条目中取出物理页号(PPN),通过将物理页号与虚拟页偏移量(VPO)结合,

得到由物理地址(PPN)和物理页偏移量(PPO)组合的物理地址。

7. 4 TLB与四级页表支持下的VA到PA的变换

下图给出了第一级、第二级或第三级页表中条目的格式。当P= 1 时(Linux中
总是如此),地址字段包含一个 40 位物理页号(PPN),它指向适当的页表的开始处。
注意,这强加了一个要求,要求物理页表 4 KB对齐。

图 7. 4. 1 第一级、第二级和第三级页表条目格式

下图给出了第四级页表中条目的格式。当P= 1 ,地址字段包括一个 40 位PPN,

它指向物理内存中某一页的基地址,这又强加了一个要求,要求物理页 4 KB对齐。

图 7. 4. 2 第四级页表条目格式

PTE有三个权限位,控制对页的访问。R/W位确定页的内容是可以读写的还是只读

的。U/S位确定是否能够在用户模式中访问该页,从而保护操作系统内核中的代码

和数据。

当MMU翻译每一个虚拟地址时,它还会更新另外两个内核缺页处理程序会用到

的位。每次访问一个页时,MMU都会设置A位,称为引用位。内核可以用这个

引用位来实现它的页替换算法。每次对一个页进行了写之后,MMU都会设置D

位,又称修改位或脏位(dirtybit)。修改位告诉内核在复制替换页之前是否必须写
回牺牲页。内核可以通过调用一条特殊的内核模式指令来清除引用位或修改位。

下图给出了 Corei 7 MMU如何使用四级的页表来将虚拟地址翻译成物理地址。 36
位VPN被划分成四个^9 位的片,每个片被用作到一个页表的偏移量。CR^3 寄存器
包含L 1 页表的物理地址。VPN 1 提供到一个L 1 PTE的偏移量,这个PTE包含L 2
页表的基地址。VPN 2 提供到一个L 2 PTE的偏移量,以此类推。

图 7. 4. 3 Corei 7 页表翻译
  1. 5 三级Cache支持下的物理内存访问

当 CPU提出访存请求后给出物理地址,然后高速缓存根据 CI 找到缓存组, 在
缓存组中根据 CT 与缓存行中的标记位进行匹配,如果匹配成功并且有效位为 1 ,
为命中,则按照块偏移对数据块中的数据进行访问,否则为不命中,向下一级缓
存中寻找数据,如果找到,则按照放置策略和替换策略替换该级缓存中的缓存块,
否则继续向下一级缓存或主存中寻找数据。

  1. 6 hello进程fork时的内存映射

虚拟内存和内存映射解释了fork函数如何为hello进程提供私有的虚拟地址空间。
fork为hello的进程创建虚拟内存,创建当前进程的的mm_struct,vm_area_struct
和页表的原样副本;两个进程中的每个页面都标记为只读;两个进程中的每个区
域结构都标记为私有的写时复制。在hello进程中返回时,hello进程拥有与调用fork
进程相同的虚拟内存,随后的写操作通过写时复制机制创建新页面。

  1. 7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行程序hello的步骤:

1 、删除已存在的用户区域

2 、映射私有区域

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

4 、设置程序计数器(PC):设置当前进程的上下文中的程序计数器,是指指向代
码区域的入口点。而下一次调度这个进程时,它将从这个入口点开始执行。

7. 8 缺页故障与缺页中断处理

DRAM 缓存不命中称为缺页,即虚拟内存中的字不在物理内存中。缺页导致

页面出错,产生缺页异常。缺页异常处理程序选择一个牺牲页,然后将目标页加

载到物理内存中。最后让导致缺页的指令重新启动,页面命中。

下面是整体的处理流程:

1 、处理器生成一个虚拟地址,并将它传送给MMU

2 、MMU生成PTE地址,并从高速缓存/主存请求得到它

3 、高速缓存/主存向MMU返回PTE

4 、PTE中的有效位是 0 ,所以MMU出发了一次异常,传递CPU中的控制到操作

系统内核中的缺页异常处理程序。

5 、缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它

换到磁盘。

6 、缺页处理程序页面调入新的页面,并更新内存中的PTE

7 、缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页

的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命

中。

7. 9 动态存储分配管理

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

图 7. 9. 1 堆

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

分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之
处在于由哪个实体来负责释放已分配的块。
1 、显式分配器:要求应用显式地释放任何已分配的块。
2 、隐式分配器:另一方面,要求分配器检测一个已分配块何时不再被程序所使用,
那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的巳分配
的块的过程叫做垃圾收集。例如,诸如Lisp、ML以及Java之类的高级语言就依

赖垃圾收集来释放已分配的块。

带边界标签的隐式空闲链表分配器管理:

带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外

填充以及一个字的尾部组成的。在隐式空闲链表中,因为空闲块是通过头部中的

大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历

整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为

特殊标记的结束块。当一个应用请求一个k字节的块时,分配器搜索空闲链表,
查找一个足够大的可以放置所请求块的空闲块。

图 7. 9. 2 使用边界标记的堆块格式

7. 10 本章小结

通过本章,我们阐述了hello的存储器地址空间、intel的段式管理、hello的页
式管理,TLB与四级页表支持下的VA到PA的变换、三级cache下的物理内存访
问,还介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺
页中断处理、动态存储分配管理等内容。学习到了虚拟内存是对主存的一个抽象。
处理器产生一个虚拟地址,在被发送到主存之前,这个地址被翻译成一个物理地
址。从虚拟地址空间到物理地址空间的地址翻译要求硬件和软件紧密合作。专门
的硬件使用页表来翻译虚拟地址。

(第 7 章 2 分)
第 8 章 hello的IO管理
  1. 1 Linux的IO设备管理方法

一个Linux文件就是一个字节的序列,所有的I/O设备(例如网络,磁盘和终
端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。
这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接
口,称为UnixI/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
我们可以对文件的操作有:打开关闭操作open和close;读写操作read和write;
改变当前文件位置lseek等

  1. 2 简述Unix IO接口及其函数

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

函数:
1 .打开和关闭文件。
打开文件函数原型:intopen(char*filename,intflags,mode_tmode)

返回值:若成功则为新文件描述符,否则返回-^1 ;

flags:O_RDONLY(只读),O_WRONLY(只写),O_RDWR(可读写)
mode:指定新文件的访问权限位。

关闭文件函数原型:intclose(fd)
返回值:成功返回 0 ,否则为- 1

2 ,读和写文件
读文件函数原型:ssize_tread(intfd,void*buf,size_tn)
返回值:成功则返回读的字节数,若EOF则为 0 ,出错为- 1
描述:从描述符为fd的当前文件位置复制最多n个字节到内存位置buf

写文件函数原型:ssize_twirte(intfd,constvoid*buf,size_tn)
返回值:成功则返回写的字节数,出错则为- 1
描述:从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置

  1. 3 printf的实现分析

先看printf函数的函数体:

  1. intprintf(constchar*fmt,…)
  2. {
  3. inti;
  4. charbuf[ 256 ];
  5. va_listarg=(va_list)((char*)(&fmt)+ 4 );
  6. i=vsprintf(buf,fmt,arg);
  7. write(buf,i);
  8. returni;
  9. }

发现里面还调用了函数vsprintf,在查看一下vsprintf的函数实现

  1. intvsprintf(charbuf,constcharfmt,va_listargs)
  2. {
  3. char*p;
  4. chartmp[ 256 ];
  5. va_listp_next_arg=args;
  6. for(p=buf;*fmt;fmt++){
  7. if(*fmt!=’%’){
  8. *p++=*fmt;
  9. continue;
  10. }

12.

  1. fmt++;
  2. switch(*fmt){
  3. case’x’:
  4. itoa(tmp,((int)p_next_arg));
  5. strcpy(p,tmp);
  6. p_next_arg+= 4 ;
  7. p+=strlen(tmp);
  8. break;
  9. case’s’:
  10. break;
  11. default:
  12. break;
  13. }
  14. }
  15. return(p-buf);
  16. }

系统函数write:

  1. write:
  2. moveax,_NR_write
  3. movebx,[esp+ 4 ]
  4. movecx,[esp+ 8 ]
  5. intINT_VECTOR_SYS_CALL

发现write里调用了syscall,查看syscall:

  1. sys_call:
  2. callsave
  3. pushdword[p_proc_ready]
  4. sti
  5. pushecx
  6. pushebx
  7. call[sys_call_table+eax* 4 ]
  8. addesp, 4 * 3
  9. mov[esi+EAXREG-P_STACKBASE],eax
  10. cli
  11. ret

分析得知:
printf函数:接受一个fmt的格式,然后将匹配到的参数按照fmt格式输出。printf
用了两个外部函数,一个是vsprintf,还有一个是write。
vsprintf函数:接受确定输出格式的格式字符串fmt(输入)。用格式字符串对个
数变化的参数进行格式化,产生格式化输出。

write函数:将buf中的i个元素写到终端。

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0 x 80 或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜
色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点
(RGB分量)。

  1. 4 getchar的实现分析

当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘
缓冲区中直到用户按回车为止,回车字符也放在缓冲区中。当用户键入回车之后,
getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的
第一个字符的ASCII码,如出错返回- 1 ,且将用户输入的字符回显到屏幕。如用
户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后
续getchar调用读取。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii
码,保存到系统的键盘缓冲区。
getchar调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车
键才返回。

8. 5 本章小结

本章主要介绍了Linux的I/O设备管理方法、UnixI/O接口及其函数,分析了printf
函数和getchar函数的实现。

(第 8 章 1 分)

结论

hello.c开始是用户编写的C语言源程序,之后它经历以下的过程:
1 、hello.c经过预编译,拓展得到hello.i文本文件
2 、hello.i经过编译,得到汇编代码hello.s汇编文件
3 、hello.s经过汇编,得到二进制可重定位目标文件hello.o
4 、hello.o经过链接,生成了可执行文件hello
5 、在shell利用./hello运行hello程序,父进程通过fork函数为hello创建进程

(^6) 、由execve函数加载运行当前进程的上下文中加载并运行新程序hello
7 、内存管理单元MMU、TLB、多级页表机制、三级cache完成对PA物理地址的

(^8) 、异常处理机制实现了对异常信号的处理
9 、hello最终被shell父进程回收,内核会收回为其创建的所有信息
感悟:
计算机真是非常巧妙的存在,内部的设计复杂且充满智慧。我现在的水平还浮于
表面,对于底层的掌握还不是特别到位,在以后的学习中不能仅仅停留在高级语
言层次,还应该深入到底层代码中。
(结论 0 分,缺失 - 1 分,根据内容酌情加分)

附件

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

生成的中间结果文件名 文件的作用

hello.i hello.c预处理后得到的文件

hello.s hello.i编译后得到的文件

hello.o hello.s汇编后的可重定位目标文件

hello-elf.txt hello.o的elf格式文件

hello hello.o经过链接后的可执行目标文件

hello-out-elf.txt hello的elf格式文件

hello-asm.txt hello的反汇编代码文件

hello-o-asm.txt hello.o的反汇编代码文件

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

参考文献

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

[ 1 ] 深入理解计算机系统

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

Logo

更多推荐