计算机系统
大作业
题 目 程序人生-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简介
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简介
根据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段
图 3-2 源程序代码1-a-1
图 3-3 汇编代码1-a-1
- if条件判断值、for循环停止条件值在.text段,运行时利用
图 3-4 源程序代码1-b-1
图 3-5 汇编代码1-b-1
图 3-6汇编代码1-b-2
因为没有全局变量,所以也没有雷同于浮点变为整形一类的类型转换
- 局部变量:局部变量i(4字节int型)在运行时生存在栈中,利用一条movl指令进行赋值,利用一条addl指令进行增一。
图 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中关系操作与控制转移操作均在一起,所以这里放在一起分析
图 3-12 源程序代码1-a-1
图 3-13 汇编代码1-a-1
je利用cmpl设置的条件码(ZF),若ZF = 0,说明argc等于5,条件不建立,若ZF = 1,说明argc不等于5(即执行程序时传入的参数个数不符合要求),条件建立。
图 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),则循环停止条件建立。
图 3-16 汇编代码2-a-1
假如条件不建立,控制转移至.L2(for循环部分,程序主体功能);假如条件建立,继承执行输出提示信息并退出。
图 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()函数以下为对每个函数的详细分析。
- 参数通报:int argc, char *argv[]
图 3-20 汇编代码1-a-1
由此可见,第一个参数通过寄存器EDI通报,第二个参数通过寄存器RSI通报,这一步将两个参数写入栈空间。
被启动函数调用,hello.s中没有体现,但为汇编器进行相干处理提供了信息。
图 3-21 汇编代码1-b-1
此部分汇编指令标记了程序入口等信息,猜测是提供给汇编器利用。
图 3-22 汇编代码1-c-1
图 3-23 汇编代码1-c-2
图 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指令调用。
- 函数返回:返回值被忽略。
图 3-28 源程序代码1-a-1
图 3-29 汇编代码1-a-1
注:利用寄存器EDI通报参数(整数值1),调用exit()函数以状态1退出。
- 函数调用:主函数通过call指令调用。
- 函数返回:函数不返回,直接退出程序。
源代码(第24行)及对应汇编代码(第50 ~ 52行):
图 3-30 源程序代码1-a-1
图 3-31 汇编代码1-a-1
注:利用atoi()(分析见下)作为参数调用sleep()函数。
- 函数调用:主函数通过call指令调用。
- 函数返回:返回值被忽略(实际休眠时间)。
- 参数通报:无。
- 函数调用:主函数通过call指令调用
图 3-32 源程序代码1-b-1
图 3-33 汇编代码1-b-1
- 参数通报: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过程
其结构分析如下:
以 16字节序列 Magic 开始,其描述了天生该文件的系统的字的大小和字节次序,ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括标识信息、文件类型、机器架构、入口地点、程序头表偏移和条目大小、节头表偏移和条目大小等信息,它是ELF文件的关键组成部分,对于理解和处理可执行文件和目标文件非常重要。
图 4-3 ELF头的相干信息
包含了文件中出现的各个节的意义,包括节的类型、位置和大小等信息。
图 4-4 节头的相干信息
图 4-5 程序头的相干信息
一个.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节的相干信息
图 4-7 .rela.eh_frame节的相干信息
符号表包含了程序中利用的变量、函数、类、结构体等符号的名称及其相干信息,如地点、大小、类型等。它是编译器和链接器在程序编译和链接过程中利用的重要数据结构全部重定位需要引用的符号都在其中声明。
图 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格式如下:
hello1.elf中的ELF头与hello.elf中的ELF头包含的信息种类根本雷同,以 描述了天生该文件的系统的字的大小和字节次序的16字节序列 Magic 开始,剩下的部分包含帮助链接器语法分析和解释目标文件的信息。hello1.elf中的根本信息未发生改变(如Magic,类别等),而类型发生改变,程序头大小和节头数量增加,而且得到了入口地点。
图 5-3 ELF头的相干信息
节头包含了文件中出现的各个节的语义,包括节的类型、位置、偏移量和大小等信息。与hello.elf相比,其在链接之后的内容更加丰富详细
图 5-4 节头的相干信息(部分)
程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信息。
图 5-5 程序头的相干信息
图 5-6 Dynamic section的相干信息
符号表符号表中生存着定位、重定位程序中符号定义和引用的信息,全部重定位需要引用的符号都在其中声明。
图 5-7 符号表的相干信息(部分)
图 5-8 重定位节的相干信息
图 5-9 其他相干信息
5.4 hello的虚拟地点空间
利用edb加载hello,查看本进程的虚拟地点空间各段信息,并与5.3对照分析说明。
图 5-10 edb加载hello结果
观察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的根本参数如下:
由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接口及其函数
一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O装备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的全部操作中标识这个文件,内核记载有关这个打开文件的全部信息。对于Shell创建的每个进程,其都有三个打开的文件:标准输入,标准输出,标准错误。
对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。雷同一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
内核开释文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
- int open(char* filename,int flags,mode_t mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,而且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算怎样访问这个文件,mode参数指定了新文件的访问权限位。
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的实现分析
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)表示...中的第一个参数。
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:
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:
/*
* 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.
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |