计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 信息安全
学 号 2022111189
班 级 2203202
学 生 李博文
指 导 教 师 史先俊
计算机科学与技术学院
2024年5月
摘 要
本文以hello.c这个程序为中心,阐述了Hello程序在Linux系统的生命周期,从hello.c依次深入研究了编译、链接、加载、运行、终止、回收的过程,从而了解hello.c文件的“一生”。并结合课程所学知识说明了Linux操作系统如何对Hello程序进行进程管理和存储管理等。本文通过在Ubuntu系统下对hello.c程序的深入研究,得以把计算机系统整个的体系串联在一起,回顾了学过的知识点,加深了对计算机系统的理解。
关键词:Hello程序;计算机系统;程序生命周期;计算机底层原理
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
Hello的P2P过程:From Program to Process,Hello的自白中提到了P2P,这里可以解释为从程序(Program)到进程(Process)的转变。这个过程涉及以下几个关键步骤:编码与存储:源代码(hello.c),这是程序的初始形态,存储在计算机的存储设备上;预处理:源代码首先经过预处理器处理,处理诸如宏定义和文件包含等指令;编译:预处理后的代码被编译器转换成汇编代码;汇编:汇编器将汇编代码转换为机器语言,生成目标代码;链接:链接器将多个目标文件和库链接成一个可执行文件;加载与执行:操作系统加载这个可执行文件,创建一个进程,分配必要的资源如内存和处理器时间,最终执行程序。
这个从程序到进程的转变,展示了计算机系统中软件与硬件的协同工作,以及操作系统在程序执行中的核心角色。
Hello的O2O过程:From Zero to Zero,Hello的自白中的O2O指的是从无到有(Zero to One),然后又回到无(One to Zero)的过程,即程序的生命周期:创建:程序从一个简单的想法或需求开始,逐步被开发和编写成代码,也就是在最开始的时候内存中并无Hello的内容;执行:通过在Shell下调用函数,系统会将hello文件载入内存,执行相关代码;终止:程序执行完成后,操作系统回收所有分配给该程序的资源,Hello的相关数据被删除,程序归于“无”。
这个过程不仅涉及到代码的执行,还包括操作系统如何管理和终止程序,确保系统资源的有效利用和程序的有序运行。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以 及开发与调试工具。
硬件:12th Gen Intel(R) Core(TM) i7-12700H CPU @ 2.70GHz
NVIDA GeForce RTX 3060 Laptop GPU
32GB RAM
1T 512GB SSD
软件:Windows11 23H2
Ubuntu 20.04.4 LTS 64位
调试工具:Visual Studio Code 64-bit;
gedit,gcc,notepad++,readelf, objdump, hexedit, edb
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名 | 功能 |
hello.i | 预处理后得到的文本文件 |
hello.s | 编译后得到的汇编语言文件 |
hello.o | 汇编后得到的可重定位目标文件 |
hello.elf | 用readelf读取hello.o得到的ELF格式信息 |
hello.asm | 反汇编hello.o得到的反汇编文件 |
hello1.elf | 由hello可执行文件生成的.elf文件 |
hello1.asm | 反汇编hello可执行文件得到的反汇编文件 |
表格 1 中间结果
1.4 本章小结
本章内容主要有对Hello的P2P,020过程的介绍,同时介绍了完成论文所用的具体硬件和软件平台,还有对中间结果生成的各种文件的简要介绍。
第2章 预处理
2.1 预处理的概念与作用
- 预处理的概念
预处理是编译过程中的第一个阶段,发生在实际编译之前。在这个阶段,预处理器会对源代码进行一系列的处理和转换,以准备好源代码供后续的编译阶段使用。例如,hello.c文件6到8行中的#include 命令会告诉预处理器读取系统头文件stdio.h,unistd.h,stdlib.h 的内容,并把这些内容直接插入到程序文本中。用实际值替换用#define定义的字符串,预处理是编译过程的第一个重要阶段,它通过处理宏定义、文件包含、条件编译等指令,为后续的编译阶段做好准备,使源代码能够被正确地转换为目标代码。这些预处理操作有助于提高代码的灵活性、可维护性和执行效率。
- 预处理的作用
在程序运行的过程中,预处理的作用主要包括以下四个方面:
1. 宏定义:处理源代码中的宏定义,例如使用#define关键字定义的宏。这些宏可以在预处理阶段被展开,以便在后续的编译阶段中使用。
2. 文件包含:处理源代码中的文件包含指令,例如#include指令。这些指令告诉预处理器在编译之前将指定的文件内容包含到当前文件中,以便在编译时一起处理。
3. 条件编译:处理条件编译指令,例如#if、#ifdef、#ifndef等。这些指令允许根据条件选择性地包含或排除代码,以便根据不同的条件生成不同的代码。
4. 注释处理:处理源代码中的注释,将注释从源代码中移除。
2.2在Ubuntu下预处理的命令
在Ubuntu系统下,进行预处理的命令为:
gcc:gcc -E hello.c -o hello.i
运行截图如下:
图 2-1 预处理过程1
不用gcc:cpp hello.c -o hello.i(以后内容均按照lab1给出的内容使用gcc)
运行截图如下:
图 2-2 预处理过程2
2.3 Hello的预处理结果解析
在Ubuntu中打开预处理之后的hello.i文件,可以看到文件的行数大幅增加,总共拓展到了3061行,hello.c中的main函数中的相关代码在hello.i程序中对应着3048行到3061行。
在main代码出现之前的主要内容是对#include <stdio.h>;#include <unistd.h>
;#include <stdlib.h>,展开时,CPP会先删除#后该行的指令(包括#本身),然后去Ubuntu系统本身的环境变量中寻找调用的文件,最后在/include中找到相关文件,如果文件中使用了#define语句,则会继续递归地进行展开,直到所有#define语句都被解释替换掉为止。除此之外,程序中的注释和多余的空白字符等也会被删除,并且一些值也会被替换。
图 2-3 预处理main部分展示
图 2-4 头文件位置
2.4 本章小结
本章主要介绍了预处理的概念以及作用,并且给出了Ubuntu系统下两种预处理的命令,对hello.c预处理产生的hello.i文件进行了解析。
第3章 编译
3.1 编译的概念与作用
- 编译的概念
编译是将高级语言源代码转换为计算机可执行代码的过程。在编译过程中,源代码经过一系列的处理和转换,最终生成可执行文件,在hello中,通过编译的过程,可以将hello.i转换为hello.s文件,其中包含的是能够转换成二进制码的汇编语言。
- 编译的作用
编译的作用主要有翻译: 编译器将高级语言源代码翻译成机器语言或者中间代码。这个过程包括词法分析、语法分析、语义分析等步骤,以便将源代码转换为计算机能够理解和执行的形式;优化: 编译器会对生成的中间代码进行优化,以提高程序的执行效率和性能。优化包括识别和消除冗余代码、重新组织代码以减少执行时间、以及利用硬件特性进行优化等;错误检查: 编译器会检查源代码中的语法错误、类型错误、逻辑错误等,并生成相应的错误和警告信息。这有助于程序员在编译阶段发现和修复代码中的问题。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
在Ubuntu系统下,进行编译的命令为:
gcc:gcc -S hello.c -o hello.s
运行截图如下:
图 3-1 编译过程
3.3 Hello的编译结果解析
- 文件结构分析
内容 | 含义 |
.file | 源文件 |
.text | 代码段 |
.global | 全局变量 |
.section .rodata | 存放只读变量 |
.align | 对齐方式 |
.type | 表示是函数类型/对象类型 |
.size | 表示大小 |
.long .string | 表示是long类型/string类型 |
表格 2 文件结构
- 数据和赋值分析
- 常量数据:
- printf函数中用到的格式字符串、输出字符串被保存在.rodata段
- 源程序代码:(第15行和第19行)
图 3-2 源程序代码1-a-1
- 汇编代码(第3 ~ 8行):
图 3-3 汇编代码1-a-1
- if条件判断值、for循环终止条件值在.text段,运行时使用
- 源程序代码:(第14行和第18行)
图 3-4 源程序代码1-b-1
- 汇编代码:(第24行和第56行)
图 3-5 汇编代码1-b-1
图 3-6汇编代码1-b-2
- 变量数据:
- 全局变量:无
- 源程序代码:
- 汇编代码:
因为没有全局变量,所以也没有类似于浮点变为整形一类的类型转换
- 局部变量:局部变量i(4字节int型)在运行时保存在栈中,使用一条movl指令进行赋值,使用一条addl指令进行增一。
- 源程序代码:(第12行和第18行)
图 3-7 源程序代码2-b-1
- 汇编程序代码:
第31行(初始化):
图 3-8 汇编代码2-b-1
第51行(增一):
图 3-9 汇编代码2-b-2
由此可见,局部变量i在赋初值后被保存在地址为%rbp-4的栈位置上。
- 算术操作分析
- 在for循环体中,对循环变量i的更新使用了++自增运算,汇编代码翻译成addl指令(4字节int型对应后缀“l”):
- 源程序代码:
图 3-10 源程序代码1-1
- 汇编代码:
图 3-11 汇编代码1-1
- 关系操作与控制转移分析
因为hello.c中关系操作与控制转移操作均在一起,所以这里放在一起分析
- 关系操作分析:
- 程序中if条件判断处
- 源程序代码:
图 3-12 源程序代码1-a-1
- 汇编代码:
图 3-13 汇编代码1-a-1
je使用cmpl设置的条件码(ZF),若ZF = 0,说明argc等于5,条件不成立,若ZF = 1,说明argc不等于5(即执行程序时传入的参数个数不符合要求),条件成立。
- 程序中for循环处
- 源程序代码:
图 3-14 源程序代码1-b-1
- 汇编代码:
图 3-15 汇编代码1-b-1
jle使用cmpl设置的条件码(ZF SF OF),若(SF^OF) | ZF = 1,说明循环终止条件不成立(变量i的值小于或等于9),若(SF^OF) | ZF = 0,则循环终止条件成立(变量i的值达到10),则循环终止条件成立。
- 控制转移分析:
- 程序中if条件判断处
- 源程序代码:同图3-12
- 汇编代码:
图 3-16 汇编代码2-a-1
如果条件不成立,控制转移至.L2(for循环部分,程序主体功能);如果条件成立,继续执行输出提示信息并退出。
- 程序中for循环处
- 源程序代码:同图3-14
- 汇编代码:
图 3-17 汇编代码2-b-1
如果条件不成立,控制转移至.L4,继续执行循环体;如果条件成立,不再跳转至循环体开始位置,继续向后执行直至退出。
- 数组/指针/结构操作分析
因为都在char *argv[]中所以放在一起分析
主函数main()的第二个参数是char *argv[](参数字符串数组指针),在argv数组中,argv[0]为输入程序的路径和名称字符串起始位置,argv[1]、argv[2]和argv[3]为其后的三个参数字符串的起始位置。汇编代码中相关的指令如下:
图 3-18 汇编代码1
这条指令将main()的第二个参数从寄存器写到了栈空间中。
图 3-19 汇编代码2
这6条指令从栈上取这一参数,并按照基址-变址寻址法访问argv[1]、argv[2]和argv[3](由于指针char*大小为8字节,分别偏移8、16、24字节来访问)。
- 函数操作分析
源代码中的函数有main()函数,printf()函数,exit()函数,sleep()函数,getchar()函数,atoi()函数以下为对每个函数的具体分析。
- main()函数:
- 参数传递:int argc, char *argv[]
- 汇编代码:
图 3-20 汇编代码1-a-1
由此可见,第一个参数通过寄存器EDI传递,第二个参数通过寄存器RSI传递,这一步将两个参数写入栈空间。
- 函数调用:
被启动函数调用,hello.s中没有体现,但为汇编器进行相关处理提供了信息。
- 汇编代码:
图 3-21 汇编代码1-b-1
此部分汇编指令标记了程序入口等信息,猜测是提供给汇编器使用。
- 函数返回:正常情况返回0,参数个数不正确返回1。
- 正常情况汇编代码:
图 3-22 汇编代码1-c-1
- 返回1情况汇编代码:
图 3-23 汇编代码1-c-2
- printf()函数:
- 参数传递:需要输出的字符串
- 源程序代码:
图 3-24 源程序代码1-a-1
- 汇编代码:
图 3-25 汇编代码1-a-1
- 源程序代码:
图 3-26 源程序代码1-a-2
- 汇编代码:
图 3-27 汇编代码1-a-2
注:从栈空间取argc[1]、argc[2],从只读数据段取格式/输出字符串,作为参数传递给printf()进行输出。
- 函数调用:主函数通过call指令调用。
- 函数返回:返回值被忽略。
- exit()函数:
- 参数传递:退出状态值(int类型)
- 源程序代码:
图 3-28 源程序代码1-a-1
- 汇编代码:
图 3-29 汇编代码1-a-1
注:使用寄存器EDI传递参数(整数值1),调用exit()函数以状态1退出。
- 函数调用:主函数通过call指令调用。
- 函数返回:函数不返回,直接退出程序。
- sleep()函数:
- 参数传递:休眠时间(int类型)
源代码(第24行)及对应汇编代码(第50 ~ 52行):
- 源程序代码:
图 3-30 源程序代码1-a-1
- 汇编代码:
图 3-31 汇编代码1-a-1
注:使用atoi()(分析见下)作为参数调用sleep()函数。
- 函数调用:主函数通过call指令调用。
- 函数返回:返回值被忽略(实际休眠时间)。
- getchar()函数:
- 参数传递:无。
- 函数调用:主函数通过call指令调用
- 源程序代码:
图 3-32 源程序代码1-b-1
- 汇编代码:
图 3-33 汇编代码1-b-1
- 函数返回:返回char类型值,在此程序中被忽略。
- atoi()函数:
- 参数传递:argv[4]指针值的副本
- 函数调用:主函数通过call指令调用。
- 源程序代码:
图 3-34 源程序代码1-b-1
- 汇编代码:
图 3-35 汇编代码1-b-1
- 函数返回:转换后字符串的整数值
3.4 本章小结
本章介绍了编译的概念与作用,同时以hello.s文件为例,介绍了编译器如何处理各个数据类型以及各类操作,详细分析了hello.s文件中与作业要求p4中有关的操作内容,验证了大部分数据、操作在汇编代码中的实现,同时完成本章内容的过程加深了我对编译阶段的理解,有助于我对知识的复习理解。
第4章 汇编
4.1 汇编的概念与作用
- 汇编的概念
汇编语言是一种符号化的低级语言,它使用助记符和符号来代表机器指令、寄存器和内存地址等,汇编是指汇编器(assembler)将以.s结尾的汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在.o 目标文件中的过程.
- 汇编的作用
汇编能将汇编语言翻译为机器语言,并将相关指令以可重定位目标程序格式保存在.o文件中,用来进行对计算机底层硬件的控制。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
在Ubuntu系统下,进行汇编的命令为:
gcc:gcc -c hello.s -o hello.o
运行截图如下:
图 4-1 汇编过程
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
首先,在shell中输入readelf -a hello.o > hello.elf 指令获得 hello.o 文件的 ELF 格式:
图 4-2 获得ELF过程
其结构分析如下:
- ELF 头(ELF Header):
以 16字节序列 Magic 开始,其描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括标识信息、文件类型、机器架构、入口地址、程序头表偏移和条目大小、节头表偏移和条目大小等信息,它是ELF文件的关键组成部分,对于理解和处理可执行文件和目标文件非常重要。
图 4-3 ELF头的相关信息
- 节头:
包含了文件中出现的各个节的意义,包括节的类型、位置和大小等信息。
图 4-4 节头的相关信息
- 程序头:没有
图 4-5 程序头的相关信息
- 重定位节.rela.text
一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置,8 条重定位信息分别是对.L0、puts 函数、exit 函数、.L1、printf 函数、atio、sleep 函数、getchar 函数进行重定位声明。
.rela.text节包含如下信息:偏移量:代表需要进行重定向的代码在.text或.data节中的偏移位置;信息:包括symbol和type两部分,其中symbol占前半部分,type占后半部分,symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的类型;类型:重定位到的目标的类型;加数:计算重定位位置的辅助信息。
图 4-6 .rela.txt节的相关信息
- 重定位节.rela.eh_frame
图 4-7 .rela.eh_frame节的相关信息
- 符号表Symbol table
符号表包含了程序中使用的变量、函数、类、结构体等符号的名称及其相关信息,如地址、大小、类型等。它是编译器和链接器在程序编译和链接过程中使用的重要数据结构所有重定位需要引用的符号都在其中声明。
图 4-8 符号表的相关信息
- 其他信息
图 4-9 其他信息
4.4 Hello.o的结果解析
先使用objdump -d -r hello.o > hello.asm 进行hello.o的反汇编的分析:
图 4-10 生成hello.asm过程
之后将生成的文件与与第3章的 hello.s文件进行对照分析,通过对比hello.asm与hello.s可知二者之间的差异:
- 分支转移:
在hello.s中,跳转指令的目标地址直接记为段名称,即je L2、jmp L3、jle L4。而在hello.asm中,跳转的目标为具体的地址,在机器代码中体现为目标指令地址与当前指令下一条指令的地址之差,如下图所示
图 4-10 分支转移区别1
图 4-11 分支转移区别2
图 4-12 分支转移区别3
- 函数调用:
在hello.s中,call之后直接跟着函数名称,而在hello.asm中,call 的目标地址是当前指令的下一条指令。
图 4-13 函数调用区别1
图 4-14 函数调用区别2
图 4-15 函数调用区别3
图 4-16 函数调用区别4
图 4-17 函数调用区别5
图 4-18 函数调用区别6
因为 hello.c 中调用的函数都是共享库中的函数,最终需要通过动态链接器作用才能确定函数的运行时执行地址,而在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其 call 指令后的相对地址设置为全0,此时,目标地址正是下一条指令,然后在.rela.text 节中为其添加重定位条目,等待静态链接进一步确定。
- hello.s中的操作数均为十进制,而hello.asm中的操作数被转换成十六进制;
- hello.s中的printf字符串等符号在hello.asm被替换成了待重定位的地址。
4.5 本章小结
本章对汇编的概念以及作用进行了简单介绍,并且完成了Ubuntu中汇编的过程,经过汇编阶段,汇编语言代码转化为机器语言,生成的可重定位目标文件(hello.o)为随后的链接阶段做好了准备,详细分析了产生的ELF文件的信息以及反汇编产生的asm文件的分析,了解了汇编语言与机器语言的异同之处。
第5章 链接
5.1 链接的概念与作用
- 链接的概念
链接是指通过链接器(Linker),将程序编码与数据块收集并整理成为一个单一文件,生成完全链接的可执行的目标文件(windows系统下为.exe文件,Linux系统下一般省略后缀名)的过程,之后点击生成的文件(或在终端./文件名)即可运行文件。
- 链接的作用
链接使分离编译成为可能,我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解成更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
在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-1 链接过程
5.3 可执行目标文件hello的格式
在Shell中输入命令 readelf -a hello > hello1.elf 生成 hello 程序的 ELF 格式文件,保存为hello1.elf
图 5-2 获得ELF过程
打开hello2.elf,分析hello的ELF格式如下:
- ELF 头(ELF Header)
hello1.elf中的ELF头与hello.elf中的ELF头包含的信息种类基本相同,以 描述了生成该文件的系统的字的大小和字节顺序的16字节序列 Magic 开始,剩下的部分包含帮助链接器语法分析和解释目标文件的信息。hello1.elf中的基本信息未发生改变(如Magic,类别等),而类型发生改变,程序头大小和节头数量增加,并且获得了入口地址。
图 5-3 ELF头的相关信息
- 节头
节头包含了文件中出现的各个节的语义,包括节的类型、位置、偏移量和大小等信息。与hello.elf相比,其在链接之后的内容更加丰富详细
图 5-4 节头的相关信息(部分)
- 程序头
程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信息。
图 5-5 程序头的相关信息
- Dynamic section
图 5-6 Dynamic section的相关信息
- 符号表
符号表符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。
图 5-7 符号表的相关信息(部分)
- 重定位节
图 5-8 重定位节的相关信息
- 其他
图 5-9 其他相关信息
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
- 打开edb,加载hello:
图 5-10 edb加载hello结果
- Dump与Symbols对比结果:
观察dump中的部分,发现程序被载入至地址0x400000~0x401260(401200)中。在该地址范围内,每个节的地址都与前一节中节对应的 Address 相同。
图 5-11 Dunp部分
图 5-12 对比结果(部分)
根据edb查看的结果,在地址空间~0x400fff中存放着与地址空间0x400000~0x401000相同的程序,之后存放的是.dynamic到.shstrtab节的内容。
5.5 链接的重定位过程分析
在Shell中使用命令objdump -d -r hello > hello1.asm生成反汇编文件hello1.asm,与第四章中生成的hello.o.asm文件进行比较,其不同之处如下:
图 5-13 获得asm过程
- 链接后函数数量增加。链接后的反汇编文件hello2.asm中,多出了puts@plt,printf@plt,getchar@plt,atoi@plt,exit@plt,sleep@plt等函数的代码。这是因为动态链接器将共享库中hello.c用到的函数加入可执行文件中。
图 5-14 链接后的函数
- 函数调用指令call的参数发生变化。在链接过程中,链接器解析了重定位条目,call之后的字节代码被链接器直接修改为目标地址与下一条指令的地址之差,指向相应的代码段,从而得到完整的反汇编代码。
图 5-15 call指令参数变化
- 跳转指令参数发生变化。在链接过程中,链接器解析了重定位条目,并计算相对距离,修改了对应位置的字节代码为PLT 中相应函数与下条指令的相对地址,从而得到完整的反汇编代码。
图 5-15 跳转指令参数变化1
图 5-16 跳转指令参数变化2
图 5-17 跳转指令参数变化3
5.6 hello的执行流程
用EDB打开hello,执行hello前添加程序参数2022111189 李博文 (手机号) (手机号%5)
图 5-18 EDB执行界面
之后从Symbols中查看调用的程序:
图 5-19 查看调用程序界面(到_end)
查找到的相关内容如下表所示:
程序名称 | 程序地址 |
hello!_init | 0x0000000000401000 |
hello!_start | 0x00000000004010f0 |
hello!main | 0x0000000000401125 |
hello!puts@plt | 0x0000000000401030 |
hello!printf@plt | 0x0000000000401040 |
hello!getchar@plt | 0x0000000000401050 |
hello!atoi@plt | 0x0000000000401060 |
hello!exit@plt | 0x0000000000401070 |
hello!sleep@plt | 0x0000000000401080 |
hello!_init_array_end | 0x0000000000403e50 |
hello!_init_array_start | - |
hello!_data_start | 0x0000000000404048 |
hello!_edata | 0x000000000040404c |
hello!_end | - |
表格 3 查找内容
5.7 Hello的动态链接分析
动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,在GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数,在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址,所以分析动态连接需要检查.got.plt的情况。
调用dl_init之前.got.plt段的内容:
图 5-20 调用之前内容
调用dl_init之后.got.plt段的内容:
图 5-21 调用之后内容
比较两张图可知GOT[1]和GOT[2]之间发生了变化,查询相关内容可知GOT[1]保存的是指向已经加载的共享库的链表地址。GOT[2]是动态链接器在ld-linux.so模块中的入口。这样,接下来执行程序的过程中,就可以使用过程链接表PLT和全局偏移量表GOT进行动态链接。
5.8 本章小结
本章中介绍了链接的概念与作用、并得到了链接后的hello可执行文件的ELF格式文本hello1.elf,据此分析了hello1.elf与hello.elf的异同.之后,再次反汇编得到了hello1.asm,将hello1.asm与hello.asm的比较,并且对动态连接前后变化的内容进行了分析,加深了我对虚拟地址空间、重定位和动态链接的理解。
第6章 hello进程管理
6.1 进程的概念与作用
- 进程的概念
进程是程序的执行实例,包括程序的代码、数据和运行时的状态(如程序计数器、寄存器、打开的文件等),每个进程都有自己的地址空间,用于存储程序的指令和数据.多个进程可以同时运行,每个进程都是相互独立的,有自己的执行流和资源。
- 进程的作用
进程允许多个程序同时执行,通过时间片轮转等调度算法,操作系统可以在多个进程之间切换执行,从而实现并发性;操作系统通过进程来管理计算机系统的资源,每个进程都有自己的资源分配和使用情况,操作系统负责协调和分配这些资源;个进程都运行在自己的地址空间中,相互之间不会干扰。这种隔离性可以保护进程的代码和数据,防止其他进程对其进行非法访问;进程之间可以通过进程间通信机制进行数据交换和协作。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 Shell-bash的作用
Shell-bash是一个交互型应用级程序,代表用户运行其他程序。它是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并把它送入内核去执行。
6.2.2 Shell-bash的处理流程
先从Shell终端读入输入的命令,然后切分输入字符串,获得并识别所有的参。若输入参数为内置命令,则立即执行;若输入参数并非内置命令,则调用相应的程序为其分配子进程并运行;若输入参数非法,则返回错误信息,处理完当前参数后继续处理下一参数,直到处理完毕。
6.3 Hello的fork进程创建过程
打开Shell,输入命令./hello 2022111189 李博文 (手机号) (手机号%5),带参数执行生成的可执行文件。
之后shell判断其不是内部指令,即会通过fork函数创建子进程。子进程与父进程近似,会得到一份与父进程用户级虚拟空间相同且独立的副本——包括数据段、代码、共享库、堆和用户栈等,父进程打开的文件,子进程也可读写。二者之间最大的不同在于PID的不同。fork函数被调用一次会返回两次,在父进程中,fork函数返回子进程的PID,在子进程中,fork函数返回0。
图 6-1 程序执行情况
6.4 Hello的execve过程
execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。只有出现错误时(例如找不到可执行目标文件hello),execve才会返回到调用程序,这里与调用一次返回两次的fork函数不同。函数在加载了hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,函数的执行过程会覆盖当前进程的地址空间,但并没有创建一个新进程。新的程序仍然有相同的PID,并且继承了调用execve函数时已打开的所有文件描述符。
6.5 Hello的进程执行
6.5.1 上下文
内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
6.5.2 进程上下文切换
在hello的运行过程中,若hello进程不被抢占,则正常执行;若被抢占,则进入内核模式,进行上下文切换,转入用户模式,调度其他进程。
6.5.3 进程时间片
一个进程执行它的控制流的一部分的每一个时间段叫做时间片,比如调用sleep函数时sleep的时间(手机号%5)。
6.5.4 进程调度
当hello调用sleep函数时,为了最大化利用处理器资源,sleep函数会向内核发送请求将hello挂起,并进行上下文切换,进入内核模式切换到其他进程,切换回用户模式运行抢占的进程。与此同时,将 hello 进程从运行队列加入等待队列,由用户模式变成内核模式,并开始计时。当计时结束时,sleep函数返回,触发一个中断,使得hello进程重新被调度,将其从等待队列中移出,并内核模式转为用户模式。
6.5.5 用户态与核心态的转换
为了保证系统安全,需要限制应用程序所能访问的地址空间范围,进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,一定程度上保证了系统的安全性。
6.6 hello的异常与信号处理
- 在程序正常运行时,打印十次提示信息,以输入回车为标志结束程序,并回收进程。
图 6-2 程序正常执行情况
- 在程序运行时按回车,会多打印几处空行,程序可以正常结束,结束后也会多几行空指令。
图 6-3 程序执行时按回车
- 按下ctrlc,Shell进程收到SIGINT信号^C,结束并回收hello进程。
图 6-4 程序执行时按ctrlc
- 按下ctrlz,Shell进程收到SIGSTP信号^Z,Shell显示屏幕提示信息[1]+ 已停止,并挂起hello进程。
图 6-5 程序执行时按ctrlz
- 对hello进程的挂起可由ps和jobs命令查看,可以发现hello进程确实被挂起而非被回收,且其job代号为1。
图 6-6 用ps和jobs命令查看进程
- 在Shell中输入pstree命令,可以将所有进程以树状图显示:
图 6-7 用pstree命令查看进程(部分)
- 输入kill命令,则可以杀死指定(进程组的)进程:
图 6-8 用kill命令杀死进程
- 输入fg 1则命令将hello进程再次调到前台执行,可以发现Shell首先打印hello的命令行命令,hello再从挂起处继续运行,打印剩下的语句。程序仍然可以正常结束,并完成进程回收。
图 6-9 用fg命令调回前台
- 乱按
在程序执行过程中乱按所造成的输入均缓存到stdin,当getchar的时候读出一个’\n’结尾的字串(作为一次输入),hello结束后,stdin中的其他字串会当做Shell的命令行输入。
图 6-10 乱按
6.7本章小结
本章主要介绍了进程的概念与作用,以及Shell-bash的基本概念。针对进程根据hello可执行文件中的具体情况分析了fork,execve函数的原理与执行过程,并且进行了hello程序带着各种参数情况下进行的各种结果。
第7章 hello的存储管理
7.1 hello的存储器地址空间
- 逻辑地址
逻辑地址是指由程序产生的与段相关的偏移地址部分,逻辑地址由选择符和偏移量两部分组成。具体而言,其为hello.asm中的相对偏移地址。
- 线性地址
逻辑地址经过段机制转化后为线性地址,其为处理器可寻址空间的地址,用于描述程序分页信息的地址。具体以hello而言,线性地址标志着 hello 应在内存上哪些具体数据块上运行。
- 虚拟地址
根据CSAPP教材,虚拟地址即为上述线性地址。
- 物理地址
CPU通过地址总线的寻址,找到真实的物理内存对应地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
为了运用所有的内存空间,Intel 8086设定了四个段寄存器,专门用来保存段地址:CS(Code Segment):代码段寄存器;DS(Data Segment):数据段寄存器;SS(Stack Segment):堆栈段寄存器;ES(Extra Segment):附加段寄存器。
当一个程序要执行时,就要决定程序代码、数据和堆栈各要用到内存的哪些位置,通过设定段寄存器CS,DS,SS来指向这些起始位置。通常是将DS固定,而根据需要修改CS。所以,程序可以在可寻址空间小于64K的情况下被写成任意大小。所以,程序和其数据组合起来的大小,限制在DS所指的64K内,这就是COM文件不得大于64K的原因。
段寄存器是因为对内存的分段管理而设置的。
计算机需要对内存分段,以分配给不同的程序使用(类似于硬盘分页)。在描述内存分段时,需要有如下段的信息:1.段的大小;2.段的起始地址;3.段的管理属性(禁止写入/禁止执行/系统专用等)。
保护模式(如今大多数机器已经不再支持):
段寄存器的唯一目的是存放段选择符,其前13位是一个索引号,后面3位包含一些硬件细节(还有一些隐藏位,此处略)。
寻址方式为:以段选择符作为下标,到GDT/LDT表(全局段描述符表(GDT)和局部段描述符表(LDT))中查到段地址,段地址+偏移地址=线性地址。
实模式:
段寄存器含有段值,访问存储器形成物理地址时,处理器引用相应的某个段寄存器并将其值乘以16,形成20位的段基地址,段基地址·段偏移量=线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(VA)到物理地址(PA)之间的转换通过对虚拟地址内存空间进行分页的分页机制完成。
通过7.2节中的段式管理过程,可以得到了线性地址/虚拟地址,记为VA。虚拟地址可被分为两个部分:VPN(虚拟页号)和VPO(虚拟页偏移量),根据计算机系统的特性可以确定VPN与VPO的具体位数,由于虚拟内存与物理内存的页大小相同,因此VPO与PPO(物理页偏移量)一致。而PPN(物理页号)则需通过访问页表中的页表条目(PTE)获取,如下图所示。
图 7-1 Hello的线性地址到物理地址的变换-页式管理
若PTE的有效位为1,则发生页命中,可以直接获取到物理页号PPN,PPN与PPO共同组成物理地址。
若PTE的有效位为0,说明对应虚拟页没有缓存到物理内存中,产生缺页故障,调用操作系统的内核的缺页处理程序,确定牺牲页,并调入新的页面。再返回到原来的进程,再次调用导致缺页的指令。此时发生页命中,获取到PPN,与PPO共同组成物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB:
每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降到1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。
多级页表:
多级页表为层次结构,用于压缩页表。这种方法从两个方面减少了内存要求。第一,如果一级页表中的一个PTE是空的,那么相应的二级页表就根本不会存在;第二,只有一级页表才需要总是在主存中,虚拟内存系统可以在需要时创建、页面调出或调入二级页表,最经常使用的二级页表才缓存在主存中,减少了主存的压力。
VA到PA的变换:
对于四级页表,虚拟地址(VA)被划分为4个VPN和1个VPO。每个VPN i都是一个到第i级页表的索引。对于前3级页表,每级页表中的每个PTE都指向下一级某个页表的基址。最后一级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问k个PTE。和只有一级的页表结构一样,PPO和VPO是相同的。
示意图如下(Core i7为例):
图 7-2 VA到PA的变换示意图
7.5 三级Cache支持下的物理内存访问
因为三级Cache的工作原理基本相同,所以在这里以L1 Cache为例,介绍三级Cache支持下的物理内存访问。
L1 Cache的基本参数如下:
- 8路64组相连
- 块大小64字节
由L1 Cache的基本参数,可以分析知:
块大小64字节→需要6位二进制索引→块偏移6位
共64组→需要6位二进制索引→组索引6位
余下标记位→需要PPN+PPO-6-6=40位
故L1 Cache可被划分如下(从左到右):
CT(40bit)CI(6bit)CO(6bit)
在7.4中我们已经由虚拟地址VA转换得到了物理地址PA,首先使用CI进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO取出相应的数据后返回。
若没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中请求数据(请求顺序为L2 Cache→L3 Cache→主存,若仍不命中才继续向下一级请求)。查询到数据之后,需要对数据进行读入,一种简单的放置策略如下:若映射到的组内有空闲块,则直接放置在空闲块中,若当前组内没有空闲块,则产生冲突(evict),采用LFU策略进行替换。
7.6 hello进程fork时的内存映射
当fork函数被父进程(shell)调用时,内核为新进程(未来加载执行hello的进程)创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有空间地址的抽象概念。
7.7 hello进程execve时的内存映射
execve函数加载并运行hello需要以下几个步骤:
- 删除已存在的用户区域
删除当前进程hello虚拟地址的用户部分中的已存在的区域结构。
- 映射私有区域
为新程序的代码、数据、bss和栈区域创建新的私有的、写时复制的区域结构。其中,代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
- 映射共享区域
若hello程序与共享对象或目标(如标准C库libc.so)链接,则将这些对象动态链接到hello程序,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器
最后,execve设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页。缺页故障属于异常类别中的故障,是潜在可恢复的错误。
缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,如果牺牲页已经被修改了,内核会将其复制回磁盘。随后内核从磁盘复制引发缺页异常的页面至内存,更新对应的页表项指向这个页面,随后返回。
缺页异常处理程序返回后,内核会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件,此次页面会命中。
图 7-3 缺页异常处理过程示意
7.9动态存储分配管理
动态内存管理的基本方法与策略介绍如下:
动态内存分配器维护着一个称为堆的进程的虚拟内存区域。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放可以由应用程序显式执行或内存分配器自身隐式执行。
具体而言,分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
下面介绍动态存储分配管理中较为重要的概念:
- 隐式链表
堆中的空闲块通过头部中的大小字段隐含地连接,分配器通过遍历堆中所有的块,从而间接遍历整个空闲块的集合。
图 7-4隐式链表的结构
- 显式链表
在每个空闲块中,都包含一个前驱(pred)与后继(succ)指针,从而减少了搜索与适配的时间。
图 7-5 显式链表的结构
- 带边界标记的合并
采取使用边界标记的堆块的格式,在堆块的末尾为其添加一个脚部,其为头部的副本。添加脚部之后,分配器就可以通过检查前面一个块的脚部,判断前面一个块的起始位置和状态。从而实现快速合并,减小性能消耗。
- 分离存储
维护多个空闲链表,其中,每个链表的块具有相同的大小。将所有可能的块大小分成一些等价类,从而进行分离存储。
7.10本章小结
本章主要进行了hello 的存储器地址空间、intel 的段式管理、hello 的页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理的介绍。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有的输入和输出都能以一种统一且一致的方式来执行:打开文件、改变当前的文件位置、读写文件、关闭文件。
8.2 简述Unix IO接口及其函数
- Unix I/O接口:
- 打开文件
一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。对于Shell创建的每个进程,其都有三个打开的文件:标准输入,标准输出,标准错误。
- 改变当前的文件位置
对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
- 读写文件
一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
- 关闭文件
内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
- Unix I/O函数:
- int open(char* filename,int flags,mode_t mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
- int close(fd)
fd是需要关闭的文件的描述符,close返回操作结果。
- ssize_t read(int fd,void *buf,size_t n)
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
- ssize_t wirte(int fd,const void *buf,size_t n)
write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
- printf函数体:
int printf(const char *fmt, ...)
{
int i;
va_list arg = (va_list)((char *)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
分析:printf函数调用了vsprintf函数,最后通过系统调用函数write进行输出;va_list是字符指针类型;((char *)(&fmt) + 4)表示...中的第一个参数。
- printf调用的vsprintf函数:
int vsprintf(char *buf, const char *fmt, va_list args)
{
char *p;
chartmp[256];
va_listp_next_arg = args;
for (p = buf; *fmt; fmt++)
{
if (*fmt != '%')
{
*p++ = *fmt;
continue;
}
fmt++;
switch (*fmt)
{
case 'x':
itoa(tmp, *((int *)p_next_arg));
strcpy(p, tmp);
p_next_arg += 4;
p += strlen(tmp);
break;
case 's':
break;
/* 这里应该还有一些对于
其他格式输出的处理 */
default:
break;
}
return (p - buf);
}
}
分析:vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出写入buf供系统调用write输出时使用。
- write系统调用:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
分析:这里通过几个寄存器进行传参,随后调用中断门int INT_VECTOR_SYS_CALL即通过系统来调用sys_call实现输出这一系统服务。
- sys_call部分:
sys_call:
/*
* ecx中是要打印出的元素个数
* ebx中的是要打印的buf字符数组中的第一个元素
* 这个函数的功能就是不断的打印出字符,直到遇到:'\0'
* [gs:edi]对应的是0x80000h:0采用直接写显存的方法显示字符串
*/
xor si,si
mov ah,0Fh
mov al,[ebx+si]
cmp al,'\0'
je .end
mov [gs:edi],ax
inc si
loop:
sys_call
.end:
ret
分析:通过逐个字符直接写至显存,输出格式化的字符串。
- 输出部分:
字符显示驱动子程序实现从ASCII到字模库到显示vram(即显存,存储 每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过 信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回;getchar调用系统函数read,发送一个中断信号,内核抢占这个进程,用户输入字符串,键入回车后(字符串和回车都保存在缓冲区内),再次发送信号,内核重新调度这个进程,getchar从缓冲区读入字符。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法和及其接口和函数,对printf函数和getchar函数的底层实现有了基本了解,了解了Unix IO在Linux系统中的应用
结论
hello程序的一生经历了如下过程:
- 预处理
将hello.c中include的所有外部的头文件头文件内容直接插入程序文本中,完成字符串的替换,方便后续处理;
- 编译
通过词法分析和语法分析,将合法指令翻译成等价汇编代码。通过编译过程,编译器将hello.i 翻译成汇编语言文件 hello.s;
- 汇编
将hello.s汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在hello.o 目标文件中;
- 链接
通过链接器,将hello的程序编码与动态链接库等收集整理成为一个单一文件,生成完全链接的可执行的目标文件hello;
- 加载运行
打开Shell,在其中键入 ./hello 2022111189 李博文 (手机号) (手机号%5)
,终端为其fork新建进程,并通过execve把代码和数据加载入虚拟内存空间,程序开始执行;
- 执行指令
在该进程被调度时,CPU为hello其分配时间片,在一个时间片中,hello享有CPU全部资源,PC寄存器一步一步地更新,CPU不断地取指,顺序执行自己的控制逻辑流;
- 访存
内存管理单元MMU将逻辑地址,一步步映射成物理地址,进而通过三级高速缓存系统访问物理内存/磁盘中的数据;
- 动态申请内存
printf 会调用malloc 向动态内存分配器申请堆中的内存;
- 信号处理
进程时刻等待着信号,如果运行途中键入ctr-c ctr-z 则调用shell 的信号处理函数分别进行停止、挂起等操作,对于其他信号也有相应的操作;
- 终止并被回收
Shell父进程等待并回收hello子进程,内核删除为hello进程创建的所有数据结构。
个人感悟部分:
- 计算机系统的知识主要都是抽象但有条理的,只要根据结构一步步学习就能慢慢理解其中奥妙。
- 在一个小小的hello程序中就能体验到计算机系统书中的大部分内容,让我深刻了解了理论要与实践相结合才能更好的学习知识。
- 感谢老师将课程与实验灵活分配,紧密结合,才让我在写这次大作业中感受到了知识的融会贯通,更深入的理解了计算机系统。
附件
文件名 | 功能 |
hello.i | 预处理后得到的文本文件 |
hello.s | 编译后得到的汇编语言文件 |
hello.o | 汇编后得到的可重定位目标文件 |
hello.elf | 用readelf读取hello.o得到的ELF格式信息 |
hello.asm | 反汇编hello.o得到的反汇编文件 |
hello1.elf | 由hello可执行文件生成的.elf文件 |
hello1.asm | 反汇编hello可执行文件得到的反汇编文件 |
参考文献
[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] Randal E.Bryant, David O'Hallaron. 深入理解计算机系统[M]. 机械工业出版社.2018.4
[8] Pianistx.printf 函数实现的深入剖析[EB/OL].2013[2021-6-9].
https://www.cnblogs.com/pianist/p/3315801.html.
[9] 梦想之家xiao_chen.ELF文件头更详细结构[EB/OL].2017[2021-6-10].
https://blog.csdn.net/qq_32014215/article/details/76618649.
[10] Florian.printf背后的故事[EB/OL].2014[2021-6-10].
https://www.cnblogs.com/fanzhidongyzby/p/3519838.html.
[11] printf函数实现的深入剖析. 博客园.
https://www.cnblogs.com/pianist/p/3315801.html
[12] read和write系统调用以及getchar的实现. CSDN博客.
https://blog.csdn.net/ww1473345713/article/details/51680017
[13] 深入理解计算机系统(原书第三版).机械工业出版社, 2016.