摘 要

为了探究一个程序在计算机的生命历程,本文结合著作CSAPP以及相关知识,以简单的hello.c为例,详细阐述了关于hello.c这一程序经历预处理(cpp)、编译(ccl)、汇编(as)从而变成可执行文件的过程;接着细致展开说明了此可执行文件是如何从shell(壳)中生成、通过fork、exeve生成子进程,再到系统通过映射为它分配存储空间、进行I/O管理。本文通过模拟hello.c在计算机中的一生,展示了程序在计算机中的紧密关系以及底层的处理方式,彰显了计算机系统独有的魅力。

关键词:计算机系统,汇编,链接,进程,I/O管理

目 录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

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

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

7.1 hello的存储器地址空间

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

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

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

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

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

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

7.9动态存储分配管理

7.10本章小结

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献


第1章 概述

1.1 Hello简介

1.1.1hello原程序

hello.c是一个已经编写好的的程序,可以看到它能进行命令行参数的输入,并且打印相关字符串等,我们要实现的即是hello的两个过程:P2P以及020。

P2P:Program to Process

这一过程将描述为hello是如何从一个程序转变为一个进程的。首先hello.c创建好后,会经过预处理器(cpp)对原文件中的#include、#define、#ifdef等进行内容插入、宏替换、条件编译等,同时删除相关注释,生成.i文件;接着会经过编译器(ccl)将.i文件生成汇编语言文件.s文件,再通过汇编器(as)生成一个二进制的可重定位目标文件,将其保存在一个.o文件中;通过链接进行符号解析、库链接、重定位等,最终生成一个可执行文件hello;最后用户在shell中输入./hello,由shell解析用户输入命令,再由操作系统(OS)调用fork函数创建一个新的子进程并调用execve函数将hello加载到该子进程的地址空间中,这样hello便成为了进程。

1.1.2生成可执行文件的步骤

020:Zreo-0 to Zreo-0

020描述了hello的一生。在经过P2P后,hello已经在进程中运行,当执行到return后看似hello已经结束,其实它变成了一个僵死进程,此时父进程会收到它正常终止的状态信号(经过调用waitpid函数捕获到其终止信号后),操作系统将最终释放该进程所占的一系列资源,此刻hello干净地结束了它的一生,不带走一片云彩~

1.2 环境与工具

硬件环境:AMD Ryzen 7 5800H CPU;16GB;RTX 3060

软件环境:Windows 10 64位;Vmware 16.2.4;Ubuntu 18.04.6 64位

开发工具:CodeBlocks 64位;vim+gcc,edb,gdb,objdump等

1.3 中间结果

hello.c

程序原文件

hello.i

预处理(cpp)后生成的文件

hello.s

编译(ccl)后生成的汇编文件

hello.o

汇编(as)后生成的可重定位目标文件

hello

链接后生成的可执行文件

hello.asm

hello.o文件经过objdump生成的反汇编文件

hello.elf

hello.o文件生成的elf文件

Hello.asm

hello文件经过objdump生成的反汇编文件

Hello.elf

hello文件生成的elf文件

1.3中间结果

1.4 本章小结

本章主要介绍了关于hello的基本情况,包括它的代码、P2P以及020过程、产生的中间结果文件;简要说明了完成其一生的环境与工具,为读者留下了初步的概念以及一些悬念。


第2章 预处理

2.1预处理的概念与作用

(1)概念:在编译过程中,预处理是编译的第一个阶段,它会根据“#”号开头的命令,完成对源代码进行一些文本替换和操作,生成.i文件。

(2)作用:预处理器通常以程序员在源代码中插入的预处理指令为基础,执行以下操作。它为进一步编译做准备,从而减少代码中的重复,提高代码的可维护性。

宏替换

在预处理阶段,预处理器会将源代码中出现的宏名替换为宏定义的代码(例如#define)。

文件包含

预处理器通过 #include指令,将一个源文件的内容嵌入到另一个源文件中,并将被包含的文件插入到 #include 指令的位置,从而将多个源文件组合成一个。

条件编译

预处理器会通过 #if、#ifdef、#ifndef等指令,条件选择性地编译代码,它允许在不同的编译条件下包含或排除代码块。

2.1预处理的主要作用

2.2在Ubuntu下预处理的命令

在Ubuntu下通过指令 gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i即可将hello.c文件首先经过预处理生成.i文件

2.2 预处理命令

2.3 Hello的预处理结果解析

打开hello.i文件我们可以发现里面的内容巨幅增加,之前的#include对应的头文件都被替换了相应的内容,并且原文件中所对应的注释和多余空白都被删除了,如下图2.3:

2.3 hello预处理结果

2.4 本章小结

本章主要介绍了预处理(cpp)的过程,在Ubuntu下通过相关指令得到并对比分析了预处理后得到的.i文件,完成了hello.c编译过程的第一步。

第3章 编译

3.1 编译的概念与作用

(1)概念:在编译过程中,编译是将预处理后的源代码翻译成汇编语言的阶段,完成从 .i 到 .s 即预处理后的文件到汇编语言程序的生成。

(2)作用:编译器将高级语言源代码转换为汇编语言,同时进行一些优化操作,为进一步翻译成机器语言做准备。包含以下操作:

词法、语法、语义分析

编译器将源代码转换为一个个的标记,并将标记流转换成语法树,验证程序是否符合语法规则,最后检查程序的语义是否正确。

优化

对程序进行各种优化,以提高程序的性能。这包括但不限于内联函数、循环展开等。

代码生成

将经过优化的中间代码翻译成目标机器的汇编语言代码。

3.1编译的主要作用

3.2 在Ubuntu下编译的命令

在Ubuntu下通过指令 gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s即可将hello.i文件经过编译器生成.s文件

3.2 编译命令

3.3 Hello的编译结果解析

3.3.1关于hello.s的所有内容

关于hello.s的一些声明

3.3.2 hello.s中的开始字段

hello采用的是AT&T汇编语法,用于x86-64架构。

(1)file “hello.c”:指定源文件名为 “hello.c”。

(2)text:开始代码段。

(3)section .rodata:定义只读数据段。

(4)align 8:将接下来的数据或指令在内存中对齐到8字节边界。

(5)LC0 .string和 LC1.string:定义两个字符串常量。

(6)text:重新定义代码段。

(7)globl main:将 “main”标记为全局可见,表示这是程序的入口点。

(8)type main, @function:指定 “main”是一个函数。

3.3.1数据

  1. 局部变量

在main函数中可以看到的局部变量有由main函数传入的两个参数argc以及argv,在main中定义的计数变量i。我们知道局部变量会被分配存储到寄存器中,而通常对于参数的传递,在64位操作系统中会按次序用%rdi、%rsi、%rdx、%rcx、%r8、%r9寄存器存储参数,其余参数存储在堆栈当中。在下图3.3.2中不难看出,argc作为第一个参数存储早%edi中,并通过movl指令移动到位于栈上相对于 %rbp偏移为 -20 的位置;argv是一个数组,它的首地址被放入位于栈上相对于 %rbp偏移为 -32的位置;而变量i存储到位于栈上相对于 %rbp偏移为 -4的位置。

3.3.2 hello.s中局部变量

参数6

参数1

局部变量

上一栈帧的ebp

返回地址

参数7

参数8

上一栈帧

3.3.1 栈帧结构

  1. 常量

从文件中可以看到有常量参与,例如字符串常量L01和L02,以及存在于系统中的立即数$4、$1等。另外也能发现在string中英文和数字可以直接显示,而汉字则通过/xxx的UTF-8编码显示。

3.3.3 hello.s中的常量

3.3.2赋值

以下是一份常用的汇编指令,我们能看到可以用来赋值的指令有mov、lea,而在hello.s中我们能看到许多关于赋值mov的指令,并且可以看到具有指定操作位数的后缀b(1B)、w(2B)、l(4B)、q(8B)。

3.3.4 hello.s中的赋值

mov

数据传输,将数据从一个地方移到另一个地方

lea

加载有效地址,用于地址计算

add,sub,mul,div

算术运算,加、减、乘、除

Inc,dec

增加和减少,用于递增和递减操作

cmp

比较,比较两个值的大小

jmp

无条件跳转,无条件转移到指定标签或地址

je, jne, jl, jg

条件跳转,根据比较结果进行条件跳转

call

调用函数,跳转到指定的过程或函数

ret

返回,从函数中返回到调用它的地方

push, pop

压栈和弹栈,用于操作栈

and, or, xor

位运算,按位与、按位或、按位异或

shl, shr

位移运算,左移和右移

test

位测试,与指定的值进行按位与操作

nop

空操作,不执行任何操作

3.3.2 常用汇编指令

3.3.3算术操作

根据上表,我们可以在hello.s中看到相应的算术运算操作,典型的例如计数变量i的自增1操作addl $1 -4(%rbp) 。

3.3.5 hello.s中的算术操作

3.3.4关系操作和控制转移

关系操作通常与控制转移合并使用,因此将其在此统一说明。在hello.s
中可以看到多处地方使用到cmp指令与jmp的一系列变式,它通常描述的是程序中的判断if语句以及循环终止条件,如下图3.3.6。

3.3.6 hello.s中的关系操作和控制转移

3.3.5数组/指针/结构操作

在3.3.1中我们事先提到了关于参数argv作为一个数组,其首地址(指针)被存储到位于栈上相对于 %rbp偏移为 -32的位置(即-32(%rbp)),这里我们可以看到argv的首地址(argv[0]的地址)首先传入%rax中,接着做了偏移量增加的操作,完成第二体指令后%rax中的值对应argv[1]的地址,以此类推,第五条指令完成后%rax存储argv[2]的地址。

3.3.7 hello.s中的数组操作

3.3.6函数操作

汇编文件中常常使用call指令进行相关的函数调用,并且函数返回调用ret指令,在下图3.3.8中我们可看到关于puts、exit、printf、atoi、sleep等函数的调用,以及返回指令ret。

3.3.8 hello.s中的函数操作

3.3.7类型转换操作

在函数调用中我们发现atoi函数实现了一个强制类型转换的作用,上图中%rdi存储了来自%rax对应的参数,并传入atoi函数中进行char到int类型的强制类型转换。

3.4 本章小结

在本章中,我们探讨了编译的概念和过程,以及通过编译器(例如ccl)将C程序转化为汇编程序(.s文件)的过程。本章重点介绍了“hello” 程序的编译结果,详细解释了.s文件中的文件内容,深度剖析了数字、字符串、数组等数据结构,以及一些常见的汇编语言操作,例如赋值、算术操作、关系操作、控制转移、函数操作等。通过对这些操作的解析,为下一章讨论汇编语言打下了基础。

第4章 汇编

4.1 汇编的概念与作用

  1. 概念:在编译过程中,汇编是将汇编语言代码翻译成机器代码的阶段,完成从 .s到 .o 即汇编语言程序到二进制可重定位目标文件的生成。

(2)作用:汇编器将汇编语言的代码翻译成机器代码,生成基于硬件架构上的指令、符号表以及重定位条目,使生成的目标文件包含正确的地址信息,以便在链接时和运行时能够正确地与其他模块连接。

4.2 在Ubuntu下汇编的命令

在Ubuntu下通过指令 gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o即可将hello.s文件经过汇编器生成.o文件

4.2 汇编命令

4.3 可重定位目标elf格式

在终端中输入指令readelf -a hello.o > hello.elf即可得到hello.o的elf文件

4.3.1 hello.o的elf文件

4.3.1ELF头

在《深入理解计算机系统》中介绍到ELF以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。在ELF头中可以看到目标文件的类型、系统架构信息、节头大小和数量等信息。

4.3.2 hello.o的elf头

4.3.2节头部表

加载ELF头与节头部表之间的都是节,在所有节中能够看到一些熟悉的节,例如.text、.data、.bss、rodata等,这些节的信息存储在节头部表中,包括名称、类型、地址、偏移量等等。这里简单地介绍一下相关的节,如下表4.3:

.text

已编译程序的机器代码

.rodata

记录只读数据,例如printf语句中的格式串和开关语句中的跳转表

.data

已经初始化的全局变量和静态变量(注:局部变量在运行时保存在栈中,不出现在.data以及.bss中)

.bss

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

.symtab

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

.rel.text

一个.text节中位置的列表

.rel.data

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

.debug

调试符号表

.line

原始源程序中的行号和.text节中机器指令之间的映射

.strtab

字符串表

4.3 常见节段

4.3.3 hello.o的节头部表

4.3.3重定位节

重定位节是可重定位目标文件的重要组成部分,我们知道在程序最终变成可执行文件前需要将一些符号进行应用和解析、同时要将一些特定的外部函数(例如c中的printf)的地址进行定位,这是链接器最后所做的,在这之前,汇编器会事先在相应的节中生成重定位条目(重定位节),以便链接器不加甄别地进行重定位操作。可以看到elf文件中.rlea.text以及.rela.eh.frame所对应的重定位条目,里面记录着一些需要重定位的符号puts、exit等以及他们的偏移量。

4.3.4 hello.o的重定位节

4.3.4符号表

符号表是由汇编器生成的,使用编译器输出到汇编语言.s中的符号。其中可以看到一些符号的偏移量(value)、大小(size)、类型(type)等信息,值得注意的是,它标识了一些需要重定位函数和变量的信息

4.3.5 hello.o的符号表

4.4 Hello.o的结果解析

在Ubuntu下根据objdump -d -r hello.o> hello.asm生成关于hello.o的反汇编内容。如下图4.4.1:

4.4.1 hello.o反汇编内容生成指令

打开反汇编文件我们可以发现其中的内容与上一章.s文件有相同又有不同。在反汇编文件中,左侧显示的是一些机器码,而右侧显示的是有过修改的汇编语言,其中立即数从原来的十进制变为十六进制,同时控制转移的跳转指令变为了关于地址计算的相对偏移地址寻址,并且一些有关的指令后缀(例如b、l)被省略了,

可以说这种变化将更加贴近机器层面的语言。

4.4.2 hello.o反汇编内容

4.5 本章小结

本章介绍了汇编器生成可重定位目标文件的过程,重点说明了该.o文件的elf格式文件的结构和内容,同时对比分析了由该.o文件生成的反汇编文件的关于立即数以及函数调用、控制转移的变化,彰显了汇编阶段的重要作用,为下一章链接打下基础。

5链接

5.1 链接的概念与作用

(1)概念:链接阶段是将多个目标文件和库文件组合成最终的可执行文件的阶段,是将各种代码和数据片段收集并组合成为一个单一文件的过程。这个阶段由链接器完成,从 hello.o 生成hello可执行文件。

(2)作用:借助符号表,有时按照符号的强弱对符号进行解析,使得每个引用都与符号表中的一个确定的符号唯一关联,同时进行重定位,完成关于节以及节中的符号引用重定位。

5.2 在Ubuntu下链接的命令

这里使用ld的链接命令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进行链接,需要注意的是应该不只连接hello.o文件。

5.2 hello链接命令

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

同样的,输入指令readelf -a hello > Hello.elf得到hello的elf文件

5.3.1 hello的elf文件

打开elf文件后可以看到其结构大致与上一章相同,不同的是其内容大幅增加,在ELF头中某些参量发生改变。

5.3.2 hello的elf头

同时新增加了程序头、dynamic段,并发现dynamic段上记录着共享库等信息

5.3.2 hello的程序头、dynamic段

最重要的是,节头部表中的节增多,重定位条目和字符表发生了更新,

5.3.3 hello节头部表

5.3.4 hello的elf文件相关更新

5.4 hello的虚拟地址空间

这里我们在Ubuntu中使用edb加载hello,能够查看本进程的虚拟地址空间各段信息。在edb的data dump中可以看到关于elf文件的二进制信息,其中非常明显的便是ELF头在0x400000处的16字开始序列。而在5.3中程序头中可以看到INTERP标志性的/lib64/ld-linux-x86-64.so.2,同时注意到其VirtAddr为0x400200,因此data dump中找到00400200,它对应着INTERP的虚拟地址。同理,关于程序头中标出的各个段的虚拟地址在datadump中都有对应。

5.4 edb查看hello的elf文件

5.5 链接的重定位过程分析

在终端下通过指令objdump -d -r hello> Hello.asm生成可执行文件hello的反汇编文件,打开后可以发现,相比之前的hello.asm此文件篇幅增长,并且在main函数之前多出了关于.init、.plt段的反汇编内容,而在.text段中除了main还有更多例如_start的反汇编内容。这说明链接器在链接的过程中,将main函数中共享库中所用到的函数成功链接到可执行文件中。

5.5.1 hello反汇编内容生成指令

5.5.2 hello反汇编内容

同时对比main函数的反汇编内容,可以清楚的看到原来在代码中标记为重定位类型R_X86_64_32以及R_X86_64_PC32的对应机器码由零值被赋予了虚拟地址,其中控制跳转指令的跳转地址用对应函数的地址加偏移量计算得出;.rodata字段进行了绝对地址的引用;函数调用进行了相对地址的引用。

5.5.3 hello与hello.o反汇编内容对比

5.6 hello的执行流程

在Ubuntu下使用edb执行hello,加载hello到_start,到call main,以及程序终止后,可以依次看到对应的函数历程以及地址。

_init

0x4004c0

puts@plt

0x4004f0

printf@plt

0x400500

getchar@plt

0x400510

atoi@plt

0x400520

exit@plt

0x400530

sleep@plt

0x400540

_start

0x400550

_dl_relocate_static_pie

0x400580

main

0x400586

__libc_csu_init

0x400610

__libc_csu_fini

0x400680

_fini

0x400684

5.6 hello的对应的函数历程以及地址

5.6 edb查看hello中对应的函数

5.7 Hello的动态链接分析

在《深入理计算机系统》中第七章关于链接中讲到动态链接时,动态链接器会重定位全局偏移量表(GOT)中的每个条目,使得它包含正确的绝对地址,结合Hello.asm中的节头部表的中的信息不难看出,我们可以通过观察got.plt节的内容变化来简单说明dl_init前后动态链接项目的变化。

5.7 edb查看hello中got.plt的虚拟地址变化

可以看到got.plt的虚拟地址为00600ff0,在运行程序前对应的内容为零值,运行hello后它的值发生了变化,说明动态链接项目都发生了变化。

5.8 本章小结

本章主要介绍了程序编译过程中的最后一步链接,重点对比上一章分析了连接器链接产物——hello可执行文件的elf文件内容以及它的反汇编文件,剖析了链接器在其中关于符号解析、重定位的作用,同时展示了hello的运行流程中相关函数的调用以及动态链接时链接项目的变化。至此,hello已经成为完整的“个体”,他将步入进程之中继续完成他的一生。

6hello进程管理

6.1 进程的概念与作用

(1)概念:进程是操作系统进行资源分配的最小单位,是一个程序的一次执行过程,是一个执行中程序的实例。

(2)作用:提供给应用程序两个关键的抽象,一是逻辑控制流,造成每个进程独占CPU使用的假象(通过OS内核的上下文切换机制提供);二是私有地址空间,造成每个进程独占内存系统的假象(OS内核的虚拟内存机制实现)。

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

我们知道Shell是一种用于与操作系统内核进行交互的命令行解释器。它是用户与操作系统之间的接口,允许用户通过命令行输入来与计算机进行交互。Shell接受用户的命令并将其解释为操作系统可以理解的指令,然后执行这些指令。

其作用有:(1)命令行解释和执行(2)文件系统操作(3)环境配置(4)进程控制(5)脚本编程(6)输入输出重定向(7)管道操作(8)通配符和正则表达式的模式匹配和搜索。

相关的处理流程为:命令输入->命令解释->命令查找->命令执行->输入显示->等待用户输入。

6.2 shell与系统的关系

6.3 Hello的fork进程创建过程

当用户在shell中输入./hello(表示运行hello可执行文件)时 ,shell首先判断解析“./hello”是否为内置指令,然后在当前目录下找到它,使用系统调用fork函数为hello创建一个子进程,并在该新进程的上下文中运行hello。值得注意的是子进程获得了一份与父进程用户级虚拟地址空间相同的独立的副本,包括代码和数据段、堆、共享库以及用户栈;同时可以读写父进程中打开的任何文件。而唯一的区别在于他与父进程的PID不同。此外,在《深入理解计算机系统》一书中也强调父进程和子进程是并发运行的独立进程。利用指令strace -f ./hello可以跟踪hello进程运行。

6.3 strace -f ./hello跟踪hello进程

6.4 Hello的execve过程

execve函数能够在当前进程的上下文中加载并运行一个新程序,同时携带参数列表argv和环境变量列表envp,值得注意的是该函数只有当出现错误时才能返回到调用程序,否则该函数调用一次从不返回。在execve函数加载了hello之后,它调用加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段(初始化为零),通过将虚拟地址空间中的页映射到hello的页大小的片,将hello中的代码和数据初始化在新的代码和数据,然后通过跳转到程序的第一条指令或入口点来运行该程序(也就是_start函数的地址),最终调用main函数。

6.5 Hello的进程执行

在分析hello进程执行的过程之前,首先来了解一下什么是上下文、时间片以及用户态和核心态。

什么是上下文?上下文指的是程序或系统当前的执行环境和状态,涵盖了程序状态、寄存器值、内存映射、文件描述符、信号处理器等多个方面的信息,用于描述程序或系统的执行环境和运行状态。在操作系统中,上下文主要分为进程上下文和中断上下文两种类型。

6.5.1 进程的上下文切换

什么是时间片?时间片是操作系统中用于实现多任务调度的一种技术。由于操作系统需要合理分配CPU时间给多个进程或线程,使它们交替执行,以实现同时运行多个任务的效果,总的CPU时间被切割为若干个短小的时间段,每个时间段被称为一个时间片。每个进程或线程在一个时间片内执行一定数量的指令,然后切换到下一个进程或线程,通过这种方式实现了多任务的调度。

什么是用户态和核心态?用户态和核心态是指在计算机系统中CPU的运行权限和访问资源的级别。在用户态下,程序只能执行有限的指令集且无法直接访问底层硬件资源,而在核心态下,具备更高的权限,可以执行特权指令和直接访问系统资源。操作系统通过在用户态和核心态之间切换来保障系统的安全性和稳定性。

6.5.2 用户态与内核态的切换

因此,当用户在shell中输入`./hello`运行可执行文件”hello”时,操作系统会创建一个新的进程,称为hello进程。在进程的上下文信息中,包含了程序的状态、寄存器值、内存映射、文件描述符等信息。这个新创建的进程首先处于用户态,只能执行一般的用户级指令。

随后,操作系统将为hello进程分配一个时间片,即一段短小的CPU执行时间。在这个时间片内,hello进程开始执行,运行相应的程序指令。如果在时间片结束前hello进程未执行完,操作系统会暂停该进程,并将控制权切换到其他处于就绪态的进程。

在进程切换的过程中,涉及用户态到核心态的转换。例如,当操作系统需要访问底层硬件资源或执行特权指令时,就会进行用户态到核心态的切换。这样的切换确保了系统的安全性和资源保护。

整个进程调度过程不断重复,多个进程在不同时间片内轮流执行,实现了多任务的效果。这样的调度机制保证了系统的高效运行和资源合理利用。

6.6 hello的异常与信号处理

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

我们知道异常是操作系统中一个重要的机制,它通过中断向量表或异常表来指定对应异常的处理程序。当异常发生时,控制权被传递到相应的异常处理程序,从而使系统能够采取适当的措施,可分为四种类型——中断(异步,一般由外部I/O设备引起,处理完成返回到下一条指令)、陷阱(同步,有意的发生,处理完成返回到下一条指令)、故障(同步,不是有意的,处理完成返回重新执行该指令或者终止)、终止(同步,非故意且不可修复,处理时终止当前程序)。可以明确的是在执行hello的过程中上述异常都有可能发生。

信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式,所有信号的产生以及处理全部是由内核完成的;包括信号的产生、发送、阻塞以及接受处理,以下是常用的信号。应当注意的是信号不是异常的一种,并且它的处理方式与异常中的xxx类似,在调用处理子程序完毕后会返回并执行下一条指令。

SIGHUP

该信号在用户终端关闭时产生,通常是发给和该终端关联的会话内的所有进程

终止

SIGINT

该信号在用户键入INTR字符(Ctrl-C)时产生,内核发送此信号送到当前终端的所有前台进程

终止

SIGQUIT

该信号和SIGINT类似,但由QUIT字符(通常是Ctrl-)来产生

终止

SIGILL

该信号在一个进程企图执行一条非法指令时产生

终止

SIGSEV

该信号在非法访问内存时产生,如野指针、缓冲区溢出

终止

SIGPIPE

当进程往一个没有读端的管道中写入时产生,代表“管道断裂”

终止

SIGKILL

该信号用来结束进程,并且不能被捕捉和忽略

暂停进程

6.6 常见信号类型与作用

以下分析各种命令在程序运行过程中的执行。例如不停乱按键盘,包括回车,Ctrl-Z,Ctrl-C等(Ctrl-z后可以运行ps jobs pstree fg kill 等命令)。

(1)正常执行:设定休眠时间为两秒执行程序。

6.6.1 进程正常执行

  1. 不停乱按键盘:可以看出shell尝试将随便输入的字符、空格、回车等做为命令解读。

6.6.2 进程时执行进行随意输入

  1. 在执行过程中按Ctrl+C:发送SIGINT信号给前台进程组中的每个进程,进行终止操作,同时发现进程组中hello已被回收。

6.6.3 进程执行时按Ctrl+ C

  1. 在执行过程中按Ctrl+Z:发送SIGTSTP信号给前台进程组中的每个进程,进行停止操作,此时发现进程组中hello标识为“已停止”的状态,输入ps指令可以看到hello仍然在进程组中,输入fg使hello继续完成执行。

6.6.4 进程执行时按Ctrl+ Z

  1. Ctrl+Z后输入pstree指令查看进程树状图

6.6.5 进程停止后查看进程树状图

  1. Ctrl+Z后输入kill指令

6.6.6 进程停止后发送kill指令

6.7本章小结

本章主要介绍了hello进程的执行流程,讲述了进程和shell的概念与作用,并且通过分析hello进程的创建、加载以及执行和描述hello可能遇到的异常与信号处理,说明了hello进程管理时操作系统对进程的调度和管理,复习了异常和信号的处理机制。至此我们已经描述了helloP2P的过程。

7hello的存储管理

7.1 hello的存储器地址空间

我们知道计算机有一套独特的寻址系统,对于每一个程序,它需要经过从逻辑地址转换到线性地址(段式操作),再从虚拟地址转换到物理地址(页式操作)的过程,这里我们结合hello程序分析一下这些奇妙的地址操作。

首先,逻辑地址指的是程序中产生的与段相关的偏移地址部分。它是相对于当前进程数据段的地址,不同于绝对物理地址。在Intel实模式下,逻辑地址和物理地址相等,因为实模式没有分段或分页机制,CPU不进行自动地址转换。而在Intel保护模式下,逻辑地址是指程序执行代码段限长内的偏移地址。

例如在hello.asm中可以看到关于puts的逻辑地址的段内偏移量为 0x1f。逻辑地址是由段号+段内偏移量构成,这个地址是相对于当前指令的偏移,通过计算跳转指令(callq)到 puts 函数的偏移得到。

7.1.1 逻辑地址对应的偏移量

而线性地址是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。一般而言,我们认为虚拟地址就是线性地址,在IA32中,虚拟地址需要经过到线性地址再到物理地址的变换,而IA6中,虚拟地址可以是逻辑地址,也是线性地址。

7.1.2 线性地址

物理地址是进程及其内容放置在主内存或硬盘中的地址,该地址不能直接由用户程序访问或查看,因此需要将逻辑地址映射到该地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址;但是如果没有启用分页机制,则线性地址就直接成为物理地址。

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

我们之前提到关于逻辑地址到线性地址的变化,它遵循一个段式管理的处理方式,具体如下图7.2.1:

7.2.1 逻辑地址到线性地址的变换

其中段选择符存放在段寄存器中,它由三个主要的字段构成,分别是索引(当前使用的段描述符在描述表中的位置)、TI(0值表示选择的是全局描述符表GDT,1值表示选择的是局部描述符表LDT)和RPL(表示CPU的当前特权级,00是最高级的内核态);而段描述表实际就是段表,由段描述符(段表项)构成,分为全局描述符表GDT、局部描述符表LDT以及中断描述符表IDT;段描述符是一种数据结构,又分为用户代码段和数据段描述符、系统控制段描述符。描述起来大致如下图7.2.2所示:

7.2.2 段式管理

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

在进行完段式管理后,操作系统将进行从线性地址(VA)到物理地址(PA)之间的变换,使用的是页式管理的机制。在概念上,虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组;磁盘上数组的内容被缓存在主存中,和存储器结构层次中其他缓存一样,磁盘上的数据被切割成块,这些块作为磁盘和主存之间的的传输单元。而VM系统将虚拟内存分割出的固定大小的块称之为虚拟页(VP),通常情况下虚拟页的大小固定为4K,而一个大小为2的k次方的页,其虚拟页和物理页偏移量都为k位。

为了能够准确地对应上虚拟页与物理页之间的存放替换,在物理内存中还存放着一个页表的数据结构,它是一个页表条目(PTE)的数组,使得虚拟空间的每个页在页表中一个固定的偏移量处都有一个PTE,它由一个有效位和n位地址字段组成。而MMU(内存管理单元)最终通过页表实现将虚拟页映射到物理页。

7.3.1 常驻内存的页表

值得注意的是,在每个PTE中还有有效位这一标识位,当虚拟页号(VPN)和虚拟页偏移量(VPO)确定后,便可定位到唯一的一个PTE条目,同时虚拟页偏移量直接对应物理页偏移量,此刻页表基址寄存器通过比较有效位来完成物理页号的对应。当有效位为0时,如果该PTE的地址字段为空NULL,说明该虚拟页未被缓存;如果不为空则说明该虚拟页已经缓存但没有被分配。当有效位是1则代表该内存已经缓存在了物理内存中,此时可以得到其物理页号(PPN)已经物理地址。

7.3.2 虚拟地址到物理地址的变换

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

首先了解一下什么是TLB以及多级页表

TLB(快表)是计算机系统中的一种高速缓存,用于加速虚拟地址到物理地址的转换过程。在采用虚拟内存的系统中,程序使用虚拟地址来访问内存,而这些虚拟地址需要经过地址翻译过程,将其映射到实际的物理地址。TLB的主要作用是缓存最近使用的虚拟地址到物理地址的映射关系,避免每次地址翻译都需要访问内存中的页表。TLB可以存储一部分页表的内容,以加速地址转换的速度,因此可以理解为MMU自己的cache。值得注意的是MMU会传入虚拟页号(VPN)给TLB,如果有2的t次方个组(通常描述为“2的t次方”路组相联),那么TLB索引(TLBI)就是由VPN的t个最低位构成的,VPN的剩余位则表示TLB标记(TLBT)。

7.4.1 TLB在CPU中的关系

多级页表是一种用于管理虚拟内存地址到物理内存地址映射的机制,主要用于支持大型虚拟地址空间。在多级页表中,页表的结构被分层组织,形成一个树状结构,而不是单一的一维数组。通过多级页表,系统可以将大型的虚拟地址空间分割成多个小块,每个小块通过一个页表级别进行映射。这样的分层结构使得系统在实际使用时只需加载和维护那些真正被使用的页表,而不是整个页表,提高了内存管理的效率,降低了内存开销。

7.4.2 二级页表

《深入理解计算机系统》中给出了关于Inter Core i7 / Linux系统下关于TLB以及四级页表的分析案例,从中可以看到,Core i7实现支持48位虚拟地址空间和52位物理地址空间,TLB为四路组相联(2的2次方),页大小为4KB(2的12次方),L1-cache的块大小为64字节(2的6次方)且是8路组相联的,一共64组(2的6次方)。

从上述信息可以经过计算得到,虚拟页偏移量(VPO)和物理页偏移量(PPO)为12位,所以虚拟页号(VPN)有36位,其中的最低两位表示TLB索引(TLBI),剩余的位则表示TLB标记(TLBT);同样的,物理页号(PPN)有40位,而为了进行物理页的翻译,其PPO被分为最低6位块偏移(CO)以及剩余6位组索引(CI);其PPN对应的40位则作为标记(CT)。

7.4.3 TLB与多级页表下虚拟地址到物理地址的映射

此刻,CPU 产生虚拟地址 VA,并将VA 传送给 MMU。MMU 使用前 36 位 VPN 作为 TLBT+TLBI向 TLB 中匹配,如果命中,则得到 PPN,同时与其和VPO组合成 PA。 如果 TLB 中没有命中,MMU 向页表中查询, VPN1确定在第一级页表中的偏移量,抽取出 PTE,如果在物理内存则继续确定第二级页表的起始地址,以此类推。最终我们可以在第四级页表中查询到 PPN并与 VPO 组合成 PA,向 TLB 中添加条目;否则引发缺页故障。

7.4.4 多级页表组织下的VPN与PPN

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

在开始时MMU从虚拟地址中抽出VPN,并检查TLB,看它是否因为前面的某个内存引用缓存了PTE的一个副本。TLB从VPN中抽取出TLB索引(TLBI)和TLB标记(TLBT),和组中条目进行匹配,并依次比较每个cache中的数据,如果其中有命中,则将缓存的数据返回给MMU,否则MMU需要从主存中取出相应的PTE。接着MMU发送物理地址给缓存,缓存从物理地址中抽取出缓存偏移量(CO)、缓存组索引(CI)以及缓存标记(CT),用CT与组中标记匹配,如果命中则读出偏移量CO处的数据字节,并将它返回给MMU,随后MMU将它返回给CPU。

7.5 三级cache的物理访存

7.6 hello进程fork时的内存映射

再看fork函数时,我们便可以清楚地看到fork函数是怎样创建一个带有自己独立虚拟地址空间的新进程了。

《深入理解计算机系统》中讲到,当fork函数被当前进程调用时,内核会为新进程创建各种数据结构,并分配给它一个唯一的PID。而为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本,同时将两个进程中的每个页面都标记为只读,并将两个进程中的每一区域结构都标记为私有的写时复制。

写时复制是在调用 `fork()` 之后,内核会将父进程中所有的内存页权限设置为只读,然后子进程的地址空间被映射到父进程的地址空间。当父子进程都只读内存时,这种共享是安全的。然而,当其中一个进程尝试写入内存时,CPU硬件检测到内存页是只读的,于是触发页异常中断。接着,内核的中断处理例程介入,复制触发异常的页,使父子进程各自拥有独立的一份数据。这样,每个进程都能够独立修改它们的数据,而不会影响到对方。

因此,当fork函数在新进程返回时,一般情况下新进程现在的虚拟内存刚好和调用fork函数时存在的虚拟内存相同,如果两个进程中的任何一个后来进行了写操作,写时复制机制就会创建新页面。

7.7 hello进程execve时的内存映射

我们知道在execve函数加载了hello之后,它调用加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段(初始化为零)。再进一步看exevce函数时不难发现,在加载hello时它会首先删除已存在的用户区域并映射私有区域(为hello的代码、数据、bss和栈创建新的区域),所有的这些新的区域都是私有的、写时复制的,其中代码和数据被映射到hello文件中的.text和.data区域;.bss和堆栈区域都将请求二进制零,映射到匿名文件。接着将共享对象动态链接到hello程序中去,然后再映射到用户虚拟地址空间中的共享区域中。最后execve函数设置当前进程上下文中的程序计数器,让它指向代码区域的入口点。

7.7 加载器对用户地址空间的映射

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

缺页故障是指程序在访问某个虚拟内存地址的时候,对应的物理页不在内存中,需要从磁盘或其他存储介质中调入。缺页故障触发了操作系统的缺页中断处理。

当缺页故障发生时,CPU会暂停当前指令的执行,将控制权交给操作系统内核,操作系统会根据缺页故障的地址信息,通过页表等数据结构找到对应的物理页,如果该物理页尚未载入内存,则进行页面调度将相应的数据载入内存,并更新页表。然后,操作系统重新启动被中断的指令,使其能够正常执行。

当Linux系统遇到缺页异常时,它会首先判断两个“合法”——虚拟地址是否合法?对内存的访问是否合法?缺页处理程序能够搜索区域结构中的链表把虚拟地址与每个区域结构中的vm_start和vm_end做出比较,如果虚拟地址不合法便会出现一个段错误从而终止该进程;同时系统会检查进程是否具有读写或者执行这个区域页面的权限,让不合法的访问触发保护异常以此来终止该进程。但在大多数情况下面临的是一个正常的缺页,此时它会选择一个牺牲页面,如果这个牺牲页面被修改过则将它交换出去,换入新的页面并更新页表;当缺页处理程序返回时CPU将重新启动缺页的指令,使得该指令再次将虚拟地址发送给MMU,此时MMU便能正常翻译虚拟地址。

7.8 Linux对缺页故障的处理

7.9动态存储分配管理

在编写程序时我们常常面临知道程序运行时才知道某些数据结构的大小,为解决这个问题,动态内存分配器将提供额外的虚拟内存。动态内存分配器维护着一个进程的虚拟内存区域,称为堆(一个请求二进制零的区域,它紧接在未初始化的数据区域后开始并向上生长)。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。

《深入理解计算机系统》中提到分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用,而空闲块可以用来分配,此时空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放。这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

一般情况下,动态内存管理通过内存分配函数(如`malloc`、`calloc`、`realloc`)和内存释放函数(如`free`)实现。程序可以根据需要动态分配内存,但也需要负责释放不再使用的内存以防止内存泄漏。策略涉及内存分配算法的选择(如首次适应、最佳适应等)、处理内存碎片的方法、使用内存池和缓存机制,以及垃圾回收等。

7.10本章小结

本章主要介绍了hello的存储器地址空间知识,详细说明了逻辑地址、线性地址(虚拟地址)、物理地址之间的变换关系以及处理机制,同时以《深入理解计算机系统》一书中关于Inter Core i7为例分析了关于TLB、多级页表和多级cache的运用,并且通过回顾fork与execve函数进一步分析了他们对内存映射的操作。最后我们介绍了缺页故障以及它的处理机制,简单讲述了关于动态存储的分配管理,体现了计算机系统关于虚拟内存的神奇运用。

8hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

Linux 的 I/O 设备管理方法基于文件系统的思想,将所有设备抽象为文件,通过文件描述符进行访问和控制。每个设备都被视为一个特殊的文件,可以使用标准的文件 I/O 操作(如 read、write、open)来进行输入输出,而设备文件通常位于 `/dev` 目录下,Linux 通过文件系统的统一性,将设备的访问、控制和管理与普通文件一致化。此外,Linux 采用设备驱动模型,通过内核中的设备驱动程序管理各类硬件,将硬件底层细节与上层应用程序隔离,使得应用程序更专注于高层逻辑而无需关心底层硬件实现。

8.2 简述Unix IO接口及其函数

I/O 接口是计算机系统中用于连接和交换信息的通道,负责计算机与外部设备之间的数据传输和控制信号传递,是连接CPU与外设之间的部件,它完成CPU与外界的信息传送。还包括辅助CPU工作的外围电路,如中断控制器、DMA控制器、定时器、高速cache。

8.2 简单的I/O接口

Unix I/O接口提供了一套用于进行输入和输出操作的函数和系统调用,这些函数和系统调用在Unix-like操作系统中(包括Linux)被广泛使用。以下是一些常见的Unix I/O函数:

open

用于打开文件,返回文件描述符

read

从文件描述符中读取数据

write

将数据写入文件描述符

close

关闭文件描述符

lseek

在文件中移动读/写指针

fcntl

对文件描述符进行各种控制操作

ioctl

提供对文件描述符的设备控制

select/poll/epoll

用于多路复用 I/O 操作

8.2 常见的Unix I/O函数

8.3 printf的实现分析

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

分析printf函数时需要从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等进行讨论

首先来看一下printf函数的内容:

  1. #ifndef_VALIST
  2. #define_VALIST
  3. typedefchar*va_list;
  4. #endif/*_VALIST*/
  5. typedefintacpi_native_int;
  6. #define_AUPBND(sizeof(acpi_native_int)-1)//入栈4字节对齐
  7. #define_ADNBND(sizeof(acpi_native_int)-1)//出栈4字节对齐
  8. #define_bnd(X,bnd)(((sizeof(X))+(bnd))&(~(bnd)))//4字节对齐
  9. #defineva_arg(ap,T)(*(T*)(((ap)+=(_bnd(T,_AUPBND)))-(_bnd(T,_ADNBND))))//按照4字节对齐取下一个可变参数,并且更新参数指针
  10. #defineva_end(ap)(void)0//与va_start成对,避免有些编译器告警
  11. #defineva_start(ap,A)(void)((ap)=(((char*)&(A))+(_bnd(A,_AUPBND))))//第一个可变形参指针
  12. #endif/*va_arg*/
  13. staticcharsprint_buf[2408];
  14. intprintf(constchar*fmt,…)
  15. {
  16. va_listargs;
  17. intn;
  18. //第一个可变形参指针
  19. va_start(args,fmt);
  20. //根据字符串fmt,将对应形参转换为字符串,并组合成新的字符串存储在sprint_buf[]缓存中,返回字符个数。
  21. n=vsprintf(sprint_buf,fmt,args);
  22. //c标准要求在同一个函数中va_start和va_end要配对的出现。
  23. va_end(args);
  24. //调用相关驱动接口,将将sprintf_buf中的内容输出n个字节到设备,
  25. //此处可以是串口、控制台、Telnet等,在嵌入式开发中可以灵活挂接
  26. if(console_ops.write)
  27. console_ops.write(sprint_buf,n);
  28. returnn;
  29. }

可以看到在printf函数中首先使用了可变参数的模式,在函数体内它调用了vsprintf函数(C 标准库中的一个函数,用于将格式化的数据输出到字符串中),此时它接收一个格式化字符串和一个变长参数列表,并按照格式化字符串的规定将数据格式化为字符串,生成显示信息;接着在判断语句中调用write底层系统函数,将数据从缓冲区写入标准输出。此时,在 Linux 中,用户空间程序通过软中断或系统调用(int 0x80或 syscall 指令)切换到内核空间执行系统调用。

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

以上是对printf函数的一个简单的分析,可以看到整个过程的关键在于用户程序如何与内核进行交互罢了。

8.4 getchar的实现分析

以下是getchar的代码:

  1. intgetchar(void){
  2. staticcharbuf[BUFSIZ];
  3. staticchar*bb=buf;
  4. staticintn=0;
  5. if(n==0)
  6. {
  7. n=read(0,buf,BUFSIZ);
  8. bb=buf;
  9. }
  10. return(–n>=0)” />unsignedchar)*bb++:EOF;
  11. }

它主要是通过调用read函数,将整个缓冲区都读到buf里,当n将缓冲区的长度赋值给n,若n>0,则返回buf的第一个元素,

它会进行异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章主要介绍了Linux中I/O的管理方法以及Unix I/O接口和相关的函数,同时简单分析了printf函数以及getchar函数的处理机制。

结论

经过对hello的P2P、020深入探讨,我们可以发现这过程首先由程序员使用高级语言编辑器完成源代码的编写,经过预处理、编译、汇编和链接等步骤,将高级语言代码转化为可执行文件。在运行时,通过fork函数为程序创建子进程,通过MMU进行虚拟内存地址到物理地址的映射,执行指令,处理信号,最后由父进程回收子进程,释放相关资源,完成程序的生命周期。整个过程涉及预处理、编译、汇编、链接、内存管理、信号处理和进程回收等多个环节。

  1. 预处理:将原程序hello.c经过预处理器生成hello.i文件;
  2. 编译:编译器将hello.i文件编译为hello.s文件,此刻被替换为汇编内容;
  3. 汇编:hello.s文件经过处理变为可重定位目标文件,附有符号表和重定位表;
  4. 链接:将可重定位目标文件变为可执行文件,进行符号解析以及重定位操作;
  5. 进程:fork创建hello的进程,execve加载hello并为其完成虚拟空间的映射;
  6. 存储管理:MMU将hello程序中的VA通过页表映射成PA,进而完成内存的分配;
  7. 回收:hello结束后shell回收子进程,释放hello所占有的内存。

深入参与计算机系统的设计与实现让我深刻体会到系统的复杂性和内在的协同工作。每个层级的设计决策都影响着整个系统的性能和稳定性,从硬件层面的指令集到操作系统的内存管理和进程调度,再到高级语言的编译和应用层的程序设计,每一环节都需要细致考虑。对系统的深刻理解不仅需要掌握广泛的知识,还需要追踪技术的发展动向,以适应不断演进的硬件和软件环境。这个过程中,对于抽象和底层的理解相辅相成,同时也激发了对系统优化和创新的热情。在整个设计与实现的过程中,对计算机系统的奥妙和巧妙之处有了更深层次的认识,也让我深感计算机科学的魅力和无尽的探索空间。


附件

hello.c

程序原文件

hello.i

预处理(cpp)后生成的文件

hello.s

编译(ccl)后生成的汇编文件

hello.o

汇编(as)后生成的可重定位目标文件

hello

链接后生成的可执行文件

hello.asm

hello.o文件经过objdump生成的反汇编文件

hello.elf

hello.o文件生成的elf文件

Hello.asm

hello文件经过objdump生成的反汇编文件

Hello.elf

hello文件生成的elf文件

参考文献

[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.

[7] 学c的小李.什么是预处理.C语言.Linux.https://blog.csdn.net/LMM1314521/article/details/127431524

[8]安迪西嵌入式.什么是shell,用途是什么.https://blog.csdn.net/Chuangke_Andy/article/details/124891455

[9] Ggggggtm.【Linux从入门到精通】上下文概念详解.https://blog.csdn.net/weixin_67596609/article/details/130669576

[10] octopusHu.逻辑地址、线性地址和物理地址的转换.https://blog.csdn.net/hfut_zhanghu/article/details/122340261

[11]猿来不是梦.C语言Printf函数深入析.C语言Printf函数深入解析_printf函数原型-CSDN博客

[12] 带带刷梧呗.【408笔记】操作系统第五章 IO管理.https://blog.csdn.net/weixin_44722861/article/details/127732795