摘 要
本文分析了一个hello程序从生成到接纳的全过程,包罗源文件的创建、预处置惩罚、编译、汇编、链接为可执行程序,以及历程的创建、在操纵系统上的运行和接纳等过程。同时研究了hello程序在计算机系统中的历程管理、存储管理、I/O管理等内容。
关键词:hello;P2P;Linux;编译;汇编;链接;历程管理;I/O管理;
目 录
第1章 概述... - 5 -
1.1 Hello简介... - 5 -
1.1.1 P2P(From Program to Process)... - 5 -
1.1.2 020(From Zero-0 to Zero-0)... - 5 -
1.2 环境与工具... - 5 -
1.2.1 硬件环境:... - 5 -
1.2.2 软件环境:... - 5 -
1.2.3 开发工具:... - 5 -
1.3 中心结果... - 6 -
1.4 本章小结... - 6 -
第2章 预处置惩罚... - 7 -
2.1 预处置惩罚的概念与作用... - 7 -
2.1.1概念:... - 7 -
2.1.2 作用:... - 7 -
2.2在Ubuntu下预处置惩罚的下令... - 7 -
2.3 Hello的预处置惩罚结果剖析... - 7 -
2.4 本章小结... - 8 -
第3章 编译... - 9 -
3.1 编译的概念与作用... - 9 -
3.1.1概念:... - 9 -
3.1.2 作用:... - 9 -
3.2 在Ubuntu下编译的下令... - 9 -
3.3 Hello的编译结果剖析... - 9 -
3.3.1 文件信息:... - 9 -
3.3.2 局部变量处置惩罚与赋值操纵:... - 10 -
3.3.3 字符串常量的处置惩罚:... - 10 -
3.3.4 算术操纵:... - 11 -
3.3.5 关系操纵:... - 11 -
3.3.6 数组操纵:... - 12 -
3.3.7 控制转移:... - 13 -
3.3.8 参数传递:... - 14 -
3.3.9 函数调用与返回:... - 14 -
3.4 本章小结... - 15 -
第4章 汇编... - 16 -
4.1 汇编的概念与作用... - 16 -
4.1.1概念:... - 16 -
4.1.2 作用:... - 16 -
4.2 在Ubuntu下汇编的下令... - 16 -
4.3 可重定位目标elf格式... - 16 -
4.3.1典范的ELF可重定位目标文件格式:... - 16 -
4.3.2 ELF头:... - 17 -
4.3.3 节头部表:... - 18 -
4.3.4 可重定位节和符号表:... - 18 -
4.4 Hello.o的结果剖析... - 19 -
4.4.1 呆板语言的构成:... - 19 -
4.4.2 分支转移:... - 20 -
4.4.3 函数调用:... - 21 -
4.5 本章小结... - 22 -
第5章 链接... - 23 -
5.1 链接的概念与作用... - 23 -
5.1.1 概念:... - 23 -
5.1.2 作用:... - 23 -
5.2 在Ubuntu下链接的下令... - 23 -
5.3 可执行目标文件hello的格式... - 23 -
5.3.1 经典的可执行目标文件的ELF格式:... - 23 -
5.3.2 ELF头:... - 24 -
5.3.3 段头部表:... - 24 -
5.3.4 节头部表:... - 25 -
5.4 hello的虚拟地点空间... - 26 -
5.5 链接的重定位过程分析... - 27 -
5.1.1 hello与hello.o的差异:... - 27 -
5.5.2重定位过程:... - 29 -
5.6 hello的执行流程... - 29 -
5.7 Hello的动态链接分析... - 30 -
5.8 本章小结... - 30 -
第6章 hello历程管理... - 31 -
6.1 历程的概念与作用... - 31 -
6.1.1 概念:... - 31 -
6.1.2 作用:... - 31 -
6.2 简述壳Shell-bash的作用与处置惩罚流程... - 31 -
6.2.1 作用:... - 31 -
6.2.2 处置惩罚流程:... - 31 -
6.3 Hello的fork历程创建过程... - 31 -
6.4 Hello的execve过程... - 32 -
6.5 Hello的历程执行... - 32 -
6.6 hello的异常与信号处置惩罚... - 32 -
6.6.1大概的异常与信号:... - 32 -
6.6.2 hello执行中大概出现的异常:... - 33 -
6.7本章小结... - 35 -
第7章 hello的存储管理... - 36 -
7.1 hello的存储器地点空间... - 36 -
7.2 Intel逻辑地点到线性地点的变更-段式管理... - 36 -
7.3 Hello的线性地点到物理地点的变更-页式管理... - 37 -
7.4 TLB与四级页表支持下的VA到PA的变更... - 37 -
7.5 三级Cache支持下的物理内存访问... - 38 -
7.6 hello历程fork时的内存映射... - 39 -
7.7 hello历程execve时的内存映射... - 40 -
7.8 缺页故障与缺页中断处置惩罚... - 40 -
7.9动态存储分配管理... - 41 -
7.10本章小结... - 41 -
第8章 hello的IO管理... - 42 -
8.1 Linux的IO设备管理方法... - 42 -
8.2 简述Unix IO接口及其函数... - 42 -
8.2.1 Unix I/O接口:... - 42 -
8.2.2 函数:... - 42 -
8.3 printf的实现分析... - 43 -
8.4 getchar的实现分析... - 44 -
8.5本章小结... - 44 -
结论... - 45 -
附件... - 46 -
参考文献... - 47 -
第1章 概述
1.1 Hello简介
1.1.1 P2P(From Program to Process)
P2P指的是hello.c从源程序到历程的过程。首先编写代码得到源文件hello.c,之后通过一系列过程转化为可执行文件。首先hello.c经过C预处置惩罚器(cpp)的编译预处置惩罚,翻译成一个ASCII码的中心文件hello.i;接下来,驱动程序运行C编译器(ccl)将hello.i翻译成一个ASCII汇编语言文件hello.s;然后驱动程序运行汇编器(as),将hello.s翻译成一个可重定位目标文件hello.o;末了,运行连接器程序(ld),将hello.o与其他一些必要的系统目标文件组合起来,创建一个可执行目标文件,即hello。
执行hello程序时,系统会创建一个历程再将hello程序加载进入,最终实现了从程序到历程的整个过程。
1.1.2 020(From Zero-0 to Zero-0)
程序运行时,shell创建一个新历程,在历程中调用execve函数将hello程序加载到相应的上下文中,并将程序内容载入物理内存。然后调用main函数。程序运行结束后,子历程成为僵死历程,父历程调用waitpid接纳历程,开释虚拟内存空间,删除干系内容。这就是hello.c的O2O过程。
1.2 环境与工具
1.2.1 硬件环境:
x64 CPU;3.20GHz;16GRAM;
1.2.2 软件环境:
windows10 64位;VMware Workstation Pro16.2.2;Ubuntu 23.04
1.2.3 开发工具:
gcc;vim;edb;objdump;CodeBlocks
1.3 中心结果
文件名
| 作用
| hello.c
| hello的C源文件
| hello.i
| hello.c预处置惩罚后得到的预编译文本文件
| hello.s
| hello.i经过编译后得到的汇编语言文本文件
| hello.o
| hello.s经过汇编得到的可重定位目标文件
| hello
| hello.o经过链接得到的可执行目标文件
| hello_o.elf
| hello.o的ELF格式文件
| hello_o_asm.txt
| hello.o反汇编后的文件
| hello.elf
| hello的ELF格式文件
| hello_asm.txt
| hello反汇编后的文件
|
1.4 本章小结
本章概括介绍了hello的P2P和O2O的过程。列举了过程中的中心文件与操纵。同时介绍了本实验用到的硬软件环境和开发调试工具。
第2章 预处置惩罚
2.1 预处置惩罚的概念与作用
2.1.1概念:
预处置惩罚是编译过程的一部门,它发生在代码现实编译之前。预处置惩罚器是编译器的一个组成部门,它会对代码举行一些预处置惩罚操纵,以生成可以交给编译器的新代码。预处置惩罚主要包罗宏更换、文件包含、条件编译、符号定义和编译器指令几个方面。
2.1.2 作用:
- 通过宏更换可以避免代码中的重复,进步代码的可读性。
- 通过文件包含可以将代码模块化,降低代码的复杂度,进步代码的可维护性。
- 通过条件编译可以根据不同的编译条件生成不同的代码,实现跨平台适配或调试控制。
- 通过符号定义可以进步代码的灵活性和可维护性。
- 通过编译器指令可以向编译器传递信息,指定编译器的优化级别、错误处置惩罚方式等。
2.2在Ubuntu下预处置惩罚的下令
在终端下令行输入gcc -E hello.c -o hello.i,生成预编译文本文件hello.i。
2.3 Hello的预处置惩罚结果剖析
hello.i是hello.c经过预处置惩罚得到的文本文件,可以看到其行数由24行扩展至3106行。前面的部门主要是一些系统头文件的引入和宏定义的睁开,我们能看到大量的typedef,布局体,以及一些系统头文件的所在地点等内容。末了我们能看到原来的hello.c文件的代码位于末尾处。
2.4 本章小结
本章首先介绍了预处置惩罚的概念与作用,接着在Ubuntu下演示了hello.c的预处置惩罚,并对得到的预编译文件hello.i举行分析。
第3章 编译
3.1 编译的概念与作用
3.1.1概念:
编译是指编译器将经过预处置惩罚的文件举行分析优化,并生成汇编语言程序的过程。
3.1.2 作用:
编译过程可将不同的高级语言程序都转化为汇编语言程序,在之后的过程中,汇编语言将被转化为对应的呆板指令,这有利于程序在不同设备平台上的移植。
3.2 在Ubuntu下编译的下令
在终端下令行输入gcc -S hello.i -o hello.s,生成汇编语言文本文件hello.s。
3.3 Hello的编译结果剖析
3.3.1 文件信息:
hello.s中最开始记录的是文件干系的信息,如图所示
此中包含的信息有:.file对应着源文件,.text表示以下代码是代码段的内容,用于存放程序的指令和函数代码,.section .rodata表示以下内容是只读数据段,.align 8表示当前内容在内存中按8字节对齐,.LC0、.LC1、.string表示字符串常量的标签和内容,.globl main和.type main, @function表示main函数的全局性和范例为函数。
3.3.2 局部变量处置惩罚与赋值操纵:
当进入main函数时,为了存储局部变量,会根据需求在栈上申请一段空间供其利用。在hello.c中,i在for循环开始时被赋值为0,在汇编语言中可以看到在.L2中,栈顶指针%rbp减4,并将立即数0存储在这个位置。
3.3.3 字符串常量的处置惩罚:
在进入main函数之前,字符串常量已在.L0和.L1处被存储,且被标记为只读范例。必要利用字符串常量时,将对应的存储地点计算后加载到%rax中,供后续利用。
3.3.4 算术操纵:
在hello.c中的for循环中,每经过一次循环,i都会加1,在汇编代码中,可以看到在for循环模块的末尾,对栈顶指针%rbp减4的位置存储的变量执行addl $1,即加1并存回该位置,对应的就是hello.c中的i++。
3.3.5 关系操纵:
关系操纵主要通过cmp指令与一些其他指令联合而实现,cmp指令与sub指令行为一样,但只根据两数之差设置条件码而不更新寄存器。在hello.c中包含着argc与4的比较,以及i和8的比较。在汇编代码中就表现为对应参数或局部变量减去比较的数,再根据设置的条件(结果等于0、小于0、小于等于0等)举行后续操纵。在hello.s中argc与4相比,相称则跳转,i与7相比,小于等于则跳转。
3.3.6 数组操纵:
对于数组的访问是通过首地点+偏移量的方式实现的。hello.c中三次访问数组argv[]的元素,都是先将-32(%rbp),即数组的首地点传入寄存器%rax中,之后根据要访问的元素加上对应的偏移量。在hello.s中访问argv[1]时就加上偏移量8,访问argv[2]时就加上偏移量16,访问argv[3]时就加上偏移量24。
3.3.7 控制转移:
程序中的控制转移主要是指条件、循环等控制程序执行次序方式的操纵。hello.c中判断argc是否等于4,i是否小于8,在hello.s中,利用cmpl比较argc与4,等于则je跳转至.L2,.L2中将i初始化后jmp跳转至.L3,在.L3中cmpl比较i,若i小于等于7,则jle跳转至.L4,执行循环体中的内容。
3.3.8 参数传递:
调用函数之前,编译器会将参数存储在寄存器中,方便调用的函数利用。
当函数参数个数不大于6个时,按照优先级次序利用寄存器传递参数:rdi,rsi,rdx,rcx,r8,r9;当参数个数大于6个时,前六个参数利用寄存器存放,其他参数压入栈中存储。由之前的图可知,前文中的argc存储在rdi中,argv[]存储在rsi中,在调用时,参数被从寄存器中取出存入已为其分配内存的栈中,等待之后的利用。
3.3.9 函数调用与返回:
函数调用时通过指令call,将当前指令运行的地点压入栈中,并执行该函数。当调用的函数执行完后弹出当前地点,继续之后的程序。函数调用时若必要参数,则前6个参数通过寄存器传递,其他参数通过栈传递。函数的返回值则存在寄存器%rax中。末了通过ret指令返回函数。本程序中的函数调用与返回如上图所示。1,2,3分别调用了printf、atoi、sleep函数,4将0存入%eax代表return 0,末了5通过ret将main函数返回。
3.4 本章小结
本章首先介绍了编译的概念和作用,然后以Ubuntu下的hello.s为例,逐步分析所得到的汇编代码,理解编译器是如何处置惩罚C语言的各种数据与操纵的。
第4章 汇编
4.1 汇编的概念与作用
4.1.1概念:
汇编过程将把编译得到的汇编程序指令逐条翻译为呆板语言指令,然后把这些指令打包成可重定位目标程序的格式,并将结果生存在一个二进制文件中。
4.1.2 作用:
将汇编指令转化为呆板可以直接读取分析的呆板指令。
4.2 在Ubuntu下汇编的下令
在终端下令行输入gcc -c hello.s -o hello.o,生成可重定位目标文件hello.o。
4.3 可重定位目标elf格式
4.3.1典范的ELF可重定位目标文件格式:
一个经典的ELF可重定位目标文件格式如图所示,主要包罗ELF头、节头部表、.text节等。
1、ELF头:以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节次序。剩下的部门包含资助链接器语法分析息争释目标文件的信息。包罗ELF头的大小、目标文件的范例、呆板范例、节头部表的文件偏移,以及节头部表中条目标大小和数目。
2、.text:已编译程序的呆板代码。
3、.rodata:只读数据。
4、.data:已初始化的全局和静态C变量。
5、.bss:未初始化的全局和静态C变量。
6、.symtab:一个符号表,存放着程序中定义和引用的函数和全局变量的信息。
7、.rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件合时,必要修改这些位置。
8、.rel.data:被模块引用或定义的所有全局变量的重定位信息。
9、.debug:一个调试符号表,其条目是程序中定义的局部变量和范例定义,程序中定义和引用的全局变量,以及原始的C源文件。
10、.line:原始C源程序中的行号和.text节中呆板指令之间的映射。
11、.strtab:一个字符串表,其内容包罗.symtab和.debug节中的符号表,以及节头部中的节的名字。
12、节头部表:描述不同节的位置和大小。
4.3.2 ELF头:
hello.o中的ELF头如下图所示:
4.3.3 节头部表:
hello.o的节头部表如下图所示:
4.3.4 可重定位节和符号表:
hello.o的可重定位节和符号表如下图所示:
4.4 Hello.o的结果剖析
4.4.1 呆板语言的构成:
通过对比hello.s及hello.o的反汇编文件可以发现,两者在汇编指令上无显著差异,但反汇编文件中包含着每条语句的呆板指令,由操纵码和地点码构成,且每条呆板指令前另有指令的相对地点。
4.4.2 分支转移:
通过对比可知hello.s中,分支转移是通过段名指定转移目标,而反汇编文件中是通过相对函数首地点的地点偏移量来指定转移目标的。
4.4.3 函数调用:
在hello.s中,函数调用是通过call指令,以函数名为目标实现调用的,而在反汇编文件中,函数调用同样是利用call指令,但调用目标是以地点偏移量给出的,且能发现目标地点是该下令的下一条下令所在的地点。阐明此时函数调用并不知道函数的具体位置,必要经过链接才能确定调用函数的准确位置。
4.5 本章小结
本章首先介绍了汇编的概念和作用。之后以hello.o为例,分析了可重定位目标的ELF格式。之后通太过析反汇编得到的hello_o_ams.txt文件,展示了呆板语言的构成,并在和hello.s的对比中,展现了呆板语言与汇编语言的联系与差异。
第5章 链接
5.1 链接的概念与作用
5.1.1 概念:
链接是将各种代码和数据片段网络并组合成为一个单一文件的过程,这个文件可以被加载到内存并执行。
5.1.2 作用:
链接可以使得分离编译成为大概。我们不用将一个大型的应用程序构造为一个巨大的源文件,而是可以把它分解成更小、更好管理的模块,可以独立修改和编译这些模块。当我们改变这些模块中的此中一个时,只需简单重新编译它,并重新链接应用,而不必重新编译其他文件。
5.2 在Ubuntu下链接的下令
在终端下令行输入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 /usr/lib/x86_64-linux-gnu/crtn.o /usr/lib/x86_64-linux-gnu/libc.so hello.o
5.3 可执行目标文件hello的格式
利用readelf -a hello > hello.elf就可以得到hello.elf文件。
5.3.1 经典的可执行目标文件的ELF格式:
可执行目标文件的格式类似于可重定位目标文件的格式。一个经典的可执行目标文件的ELF格式如下图,包罗ELF头、段头部表、节头部表,多出了.init节等。
5.3.2 ELF头:
查看hello的ELF头,发现hello的ELF头中范例处显示的是EXEC,表示是可执行目标文件,这与hello.o不同
5.3.3 段头部表:
ELF可执行文件被筹划得很轻易加载到内存,可执行文件的连续的片被映射到连续的内存段。段头部表(又称程序头部表)描述了这种映射关系,主要包罗扩所指段的范例、段的读写权限、对齐方式等。
5.3.4 节头部表:
hello的节头部表如下图所示,主要描述文件的节,包罗名称、范例、大小、地点、偏移量等信息。
5.4 hello的虚拟地点空间
利用edb加载hello,可在Data Dump查看本历程的虚拟地点空间各段信息。
从图中可以看出hello的虚拟空间从0x401000开始,且是.init的地点。对比程序头部表可知结果符合。
在节头部表中可以找到.text节的起始地点为0x4010f0,也可以找到
5.5 链接的重定位过程分析
5.1.1 hello与hello.o的差异:
首先,利用objdump -d -r hello对hello举行反汇编,得到反汇编文件hello_asm.txt,接着比较hello与hello.o。
在图中可以看出hello.o只有main函数中的指令,而hello的反汇编代码中包罗.init等多个节中的代码指令。且在hello.o中,跳转和函数调用的目标是用地点偏移量表示的,且呆板代码中并没有指向现实的函数,而在hello中,函数调用的目标是用该函数在虚拟内存中的地点来表示的,且在呆板代码中的操纵码后,即是调用函数的现实地点。
5.5.2重定位过程:
这里以sleep函数为例,未重定位之前call下令的地点码均为0,而在hello中调用sleep函数的call指令所在地点为0x4011a2,而下一条下令的地点为0x4011a7,由图可知sleep函数所在的首地点为0x4010e0,将其减去原本的下一条下令的地点即可得到0xffffff39,按小端法排列即为call指令中的地点码。从而实现了重定位的过程。
5.6 hello的执行流程
子程序名
| 子程序地点
| _init
| 0x0000000000401000
| _start
| 0x00000000004010f0
| __libc_start_main
| 0x0000000000403fd8
| __gmon_start__
| 0x0000000000403fe0
| _dl_relocate_static_pie
| 0x0000000000401120
| puts@plt
| 0x0000000000401090
| exit@plt
| 0x00000000004010d0
| printf@plt
| 0x00000000004010a0
| atoi@plt
| 0x00000000004010c0
| sleep@plt
| 0x00000000004010e0
| getchar@plt
| 0x00000000004010b0
| _fini
| 0x00000000004011c0
|
5.7 Hello的动态链接分析
动态的链接器在正常工作时采取了延迟绑定的计谋,将过程地点的绑定推迟到第一次调用该过程时,利用偏移量表GOT和过程链接表PLT的协同工作实现函数的动态链接。如图所示hello程序调用了6个共享库的函数,.plt节中就有6处跳转指令,它们跳转到了同一个地点0x401020。
5.8 本章小结
本章首先介绍了链接的概念和作用,接着以hello.o为例链接生成了hello,并对可执行目标文件举行了分析,介绍了虚拟空间地点。同时分析了重定位过程、动态链接等过程。
第6章 hello历程管理
6.1 历程的概念与作用
6.1.1 概念:
历程的经典定义就是一个执行中的程序的实例。系统中的每个程序都运行在某个历程的上下文中。上下文是由程序准确运行所需的状态组成的。这个状态包罗存放在内存中的程序的代码和数据,它的栈、通用目标寄存器的内容、程序计数器、环境变量、以及打开文件描述符的集合。
6.1.2 作用:
1.为每个程序提供了一种假象,好像程序在独占地利用处置惩罚器。
2.为每个程序提供一种假象,好像它独占地利用系统地点空间。
6.2 简述壳Shell-bash的作用与处置惩罚流程
6.2.1 作用:
shell是一种交互型的应用级程序。它能够吸收用户下令,然后调用相应的应用程序,代表用户运行其他程序。
6.2.2 处置惩罚流程:
当用户在下令行界面输入下令后,shell会剖析用户输入,识别出下令自己和大概的参数或选项。如果下令是一个内置下令则立即执行,如果是其他程序,则会用fork函数创建一个新的子历程并运行。之后在父历程中执行waitpid函数等待子历程运行结束,并将其接纳。
6.3 Hello的fork历程创建过程
父历程通过调用fork函数创建一个新的运行的子历程。新创建的子历程几乎但不完全与父历程雷同。子历程得到与父历程用户级虚拟地点空间雷同的(但是独立的)一份副本,包罗代码和数据段、堆、共享库以及用户栈。子历程还得到与父历程任何打开文件描述符雷同的副本,这就意味着当父历程fork时,子历程可以读取父历程中打开的任何文件。父历程和新创建的子历程之间最大的区别在于它们有不同的PID。
以运行hello为例,当输入./hello时,父历程为shell,首先对输入的下令举行剖析,由于不是内置下令,shell要执行该可执行文件,故调用fork函数创建一个新的子历程。
6.4 Hello的execve过程
execve函数会在当前历程的上下文中加载并运行一个新程序,它加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。execve加载了filename后,调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数。
6.5 Hello的历程执行
一个历程和其它历程轮番举行的概念称为多使命,一个历程执行它的控制流的一部门的每一时间段叫做该历程的时间片,操纵系统内核利用一种称为上下文切换的异常控制流实现多使命。内核为每一个历程维持一个上下文。上下文就是内核重新启动一个被抢占的历程所必要的状态。它由一些对象的值组成,这些对象包罗通用目标寄存器、用户栈、内核栈等。
上图展示了历程A和历程B的相互切换过程。而用户模式和内核模式的切换则依赖于处置惩罚器提供的特殊机制。处置惩罚器通常是用某个控制寄存器中的一个模式位来提供这种功能。该寄存器描述了历程当前享有的特权。当设置了模式位时,历程运行在内核模式中,此时历程可以执行指令集中的任何指令,且可以访问系统中的任何内存位置。没有设置模式位时,历程就运行在用户模式中。
6.6 hello的异常与信号处置惩罚
6.6.1大概的异常与信号:
hello执行过程中大概出现四类异常:中断、陷阱、故障和终止。具体如下图:
大概产生的信号有SIGINT、SIGQUIT、SIGKILL、SIGTERM、SIGALRM、SIGCHLD、SIGSTOP等。
6.6.2 hello执行中大概出现的异常:
1.乱按包罗回车
输入回车对程序处置惩罚并不影响,即程序未因回车出现异常,最终乱按的下令和回车只会显示在终端上。
2.运行时输入Ctrl-C
运行时输入Ctrl-C会向hello历程发送信号SIGINT使程序立即终止。
3.运行时输入Ctrl-Z
运行时输入Ctrl-Z会发送一个SIGTSTP信号给hello历程,使当前历程暂停挂起。
4.输入ps
输入ps下令可以监视背景其它的历程。
5.输入jobs
输入jobs可显示当前在背景运行的作业列表。
6.输入pstree
输入pstree下令,会将所有历程以树状图显示。
7.输入fg
输入fg,可向克制的历程发送SIGCONT信号,使其重新在前台运行。
8.输入kill
输入kill,-9表示给历程9183发送9号信号,即SIGKILL,杀死历程。
6.7本章小结
本章首先介绍了历程的概念和作用,然后介绍了shell的作用与处置惩罚流程,接着分析了hello的fork历程创建过程、execve过程、历程执行等过程。末了在Ubuntu上尝试了hello在执行过程中的异常与不同信号的处置惩罚。
第7章 hello的存储管理
7.1 hello的存储器地点空间
逻辑地点:逻辑地点是程序经过编译后出现在汇编代码中的地点,用来指定一个操纵数或者是一条指令的地点。
线性地点:线性地点也称为虚拟地点,是指经过逻辑地点到物理地点转换之后得到的地点。
虚拟地点:虚拟地点是一个抽象的地点,它提供给历程利用,不代表真实的物理存储器中的位置。虚拟地点是相对于历程地点空间的,它由逻辑地点或线性地点生成。虚拟地点在内存管理单元(MMU)中被转换为物理地点,然后才能访问现实的物理存储器。
物理地点:物理地点是计算机系统中现实的存储器地点。它是指计算机中存储设备中某个特定位置的物理位置。当处置惩罚器通过地点转换机制将逻辑地点或线性地点转换为物理地点后,才能在物理存储器中访问对应的数据或指令。
7.2 Intel逻辑地点到线性地点的变更-段式管理
段式管理是一种存储器管理方案,将内存分为多个段(segment),每个段都有一个段描述符(segment descriptor)来描述它的属性和基地点。每个段的大小可以不同,并且可以重叠。
逻辑地点由两部门组成:段选择子(Segment Selector)和偏移量(Offset)。段选择子包含了段描述符的索引和权限信息,偏移量指定了在段内的偏移量。以下是逻辑地点到线性地点的变更过程:
1.根据段选择子索引,从全局描述符表(GDT)或局部描述符表(LDT)中获取对应的段描述符。
2.检查段描述符的合法性和访问权限。如果段描述符无效或访问权限不符合要求,会触发异常。
3.根据段描述符中的段基地点(Segment Base Address)和偏移量,计算出线性地点:虚拟地点(VA) = 段基地点(BA) + 段内偏移量(S)
4. 根据线性地点举行内存访问操纵(如读取或写入数据)。
7.3 Hello的线性地点到物理地点的变更-页式管理
页式管理将线性地点空间分割为固定大小的虚拟页(VP),将物理地点空间分割为雷同大小的物理页(PP)。在页式管理中,线性地点到物理地点的变更是通过页表(Page Table)来实现的。具体过程如图所示,首先线性地点(虚拟地点)可以分成虚拟页号(VPN)和虚拟页偏移量(VPO)两部门,而物理地点经太过页管理也分成物理页号(PPN)和物理页偏移量(PPO)两部门。此中MMU可以利用VPN找到对应的PPN,而VPO和PPO是雷同的。故将找到的PPN与PPO联合就得到了物理地点。
7.4 TLB与四级页表支持下的VA到PA的变更
快表(TLB)是一个小的、虚拟寻址的缓存。此中每一行都生存着一个由单个页表条目(PTE)组成的块。TLB通常有着高度的相联度。用于组选择和行匹配的索引和标记字段是从虚拟地点中的虚拟页号中提取出来的。如果TLB有T=2t个组,那么TLB索引(TLBI)是由VPN的t个最低位组成的,而TLB标记(TLBT)是由VPN中剩余的位组成的。
为了办理单独一个页表在虚拟地点空间引用后存在的页表占用内存过多的题目,常通过利用层次布局的页表来压缩页表,即利用多级页表。以四级页表为例,将虚拟地点分为4个VPN和1个VPO,每个VPN都是到对应级的页表的索引,而每一级页表中的每个PTE都会指向下一级的某个页表的基址。第四级页表中的每个PTE都包含某个物理页的PPN或者一个磁盘块的地点。根据VPN依次访问第1-4级页表就可以找到对应的PPN,找到对应物理地点。
两者相联合后VA到PA的变更过程如下图所示:
7.5 三级Cache支持下的物理内存访问
现在常见CPU有三级Cache,即L1、L2、L3,且与主存多接纳组相联映射。高速缓存是一个高速缓存组的数组。每个组包含一个或多个行,每个行包含一个有效位,一些标记位,以及一个数据块。高速缓存的布局将m个地点位划分成了t个标记位、s个组索引位和b个块偏移位,如下图所示。
物理地点首先会访问L1 cache,如果掷中就访存乐成,如果没掷中会再访问L2 cache,如果还没掷中则会访问L3 cache,如果三级cache都没掷中则会直接访问主存,如果没有缺页则访存乐成,如果发生缺页会调用缺页异常处置惩罚程序把数据从硬盘调入主存再次访问主存。
7.6 hello历程fork时的内存映射
在shell中输入下令./hello后,内核调用fork函数创建子历程,为hello程序的运行创建上下文,并分配一个与父历程不同的唯一的PID。为了给这个新历程创建虚拟内存,它创建了当前历程的mm_struct、区域布局和页表的原样副本。它将两个历程中的每个页面都标记为只读,并将两个历程中的每个区域布局都标记为私有的写时复制。
当fork在新历程中返回时,新历程现在的虚拟内存刚好和调用fork时存在的虚拟内存雷同。当这两个历程中的任何一个后来举行写操纵时,写时复制机制就会创建新页面。因此,也就为每个历程保持了私有地点空间的抽象概念。
7.7 hello历程execve时的内存映射
execve函数在当前历程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当出息序。加载并运行hello必要以下几个步调:
- 删除已存在的用户区域。删除当前历程虚拟地点的用户部门中的已存在的区域布局。
- 映射私有区域。为新程序的代码、数据bss和栈区域创建新的区域布局。所有这些新的区域都是私有的、写时复制的。
- 映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地点空间中的共享区域内。
- 设置程序计数器(PC)。execve做的末了一件事就是设置当前历程上下文中的程序计数器,使之指向代码区域的入口点。
下一次调治该历程时,它将从这个入口点开始执行。Linux将根据必要换入代码和数据页面。
7.8 缺页故障与缺页中断处置惩罚
DRAM缓存不掷中称为缺页(page fault)。当地点翻译硬件从内存中读取一个虚拟地点时,若访问合法,但对应页表的有效位为0,则代表该页并未生存在主存中,触发一个缺页异常。缺页异常调用内核中的缺页异常处置惩罚程序,该程序会选择一个牺牲页,修改该牺牲页的页表条目,接着内核从磁盘复制必要的页到先前主存中的访问位置,重新启动导致缺页的指令,把导致缺页的虚拟地点重发送到地点翻译硬件。此时目标页已经缓存在主存中了,页掷中也能由地点翻译硬件正常处置惩罚了。
7.9动态存储分配管理
动态内存分配器维护着一个历程的虚拟内存区域,称为堆(heap)。系统之间细节不同,但是不失通用性,假设堆是一个哀求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地点)。对于每个历程,内核维护着一个变量brk(读做“break”),它指向堆的顶部。
分配器将堆视为一组不同大小的块(clock)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。
分配器有两种根本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责开释已分配的块。
显式分配器:要求应用显式地开释任何已分配的块。例如C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来开释一个块。
隐式分配器:另一方面,要求分配器检测一个已分配块何时不再被程序利用,那么就开释这个块。
7.10本章小结
本章主要介绍了hello的存储管理。包罗从虚拟地点到物理地点的变更过程,TLB与四级页表支持下的VA到PA的变更,运行hello历程fork和execve时的内存映射,缺页故障与缺页中断处置惩罚,末了介绍了动态存储的分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
一个Linux文件就是一个m个字节的序列:B0,B1,…,Bk,…,Bm-1。
所有的I/O设备都被模型化为文件,而所有的输入输出都被当尴尬刁难相应的文件的读和写来执行。这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,成为Unix I/O。
8.2 简述Unix IO接口及其函数
8.2.1 Unix I/O接口:
Unix I/O使得所有的输入和输出都能以一种统一且一致的方式来执行:
- 打开文件。应用程序会通过要求内核打开对应的文件,来宣告它想要访问一个I/O设备,而内核会返回一个描述符,用来在后续操纵中标识这个文件。
- 改变当前的文件位置。对于每个打开的文件,内核保持一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操纵,显式地设置文件的当前位置为k。
- 读写文件。一个读操纵就是从文件文件复制n > 0个字节到内存,从当前文件位置k开始,然后将k增加到k + n。类似地,写操纵就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
- 关闭文件。当应用完成了对文件的访问之后,它就关照内核关闭这个文件。作为相应,内核开释文件打开时创建的数据布局,并将这个描述符规复到可用的描述池中。无论一个历程由于何种缘故原由终止时,内核都会关闭所有打开的文件并开释它们的内存资源。
8.2.2 函数:
1.open函数:int open(char *filename,int flags,mode_t node);
将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在历程中当前没有打开的最小描述符。flags 参数指明了历程筹划如何访问这个文件,此外,flags参数也可以是一个或多更多位掩码的或,为写提供一些额外的指示。mode参数指定了新文件的访问权限位。
2.close函数:int close(int fd);
关闭一个打开的文件,当关闭已关闭的描述符会堕落。
3.read函数:ssize_t read(int fd,void *buf,size_t n);
从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是现实传送的字节数目。
4.write函数:ssize_t write(int fd,const void *buf,size_t n);
从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
5. lseek函数:off_t lseek(int fd, off_t offset, int whence);
应用程序显示地修改当前文件的位置。
8.3 printf的实现分析
printf函数的函数体:
在形参列表里有这么一个token:...
这个是可变形参的一种写法。当传递参数的个数不确定时,就可以用这种方式来表示。(char*)(&fmt) + 4) 表示的是...中的第一个参数,该语句即令字符指针arg指向可变形参的第一个参数。vsprintf的作用就是格式化。它接受确定输特别式的格式字符串fmt。用格式字符串对个数变革的参数举行格式化,产生格式化输出。返回的是要打印出来的字符串的长度,并将其赋值给i。末了调用write函数把buf中的i个元素的值输出到终端。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar函数的函数体:
当程序调用getchar时。程序就等着用户按键。getchar等调用read系统函数,通过系统调用读取按键ascii码,用户输入的字符被存放在键盘缓冲区中。直到接受到回车键才返回。当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符。
异步异常-键盘中断的处置惩罚:键盘中断处置惩罚子程序。接受按键扫描码转成ascii码,生存到系统的键盘缓冲区。
8.5本章小结
本章主要介绍了Linux的I/O设备管理方法、Unix I/O接口及其干系函数,并分析了printf函数和getchar函数的实现。
结论
首先,经过程序员的编写,得到了源程序hello.c。
接着,hello.c经过预处置惩罚,得到了hello.i,在这个过程中预处置惩罚器会对代码举行一些预处置惩罚操纵,以生成可以交给编译器的新代码。主要包罗宏更换、文件包含、条件编译、符号定义和编译器指令几个方面。
之后hello.i经过编译器编译得到汇编语言程序hello.s,
紧接着,hello.s经过汇编器将汇编语言程序逐条翻译成呆板指令,把这些指令打包成可重定位目标程序的格式,并将结果生存在一个二进制文件中,得到hello.o。
末了hello.o在链接器的作用下,与其他所必要调用的库函数通过动态链接生成可执行目标文件hello,至此完成了从源程序hello.c到可执行程序hello的蜕变。
在运行可执行程序hello的过程中,先在shell中输入下令./hello,接着shell剖析输入的下令并调用fork生成一个新的子历程,接着用execve将历程映射到虚拟内存中。在执行一条条指令的过程中,CPU发送的逻辑地点经过段式管理、页式管理后变更为物理地点,并在三级cache中实现访存。
在程序的执行过程中,大概会触发异常或吸收信号,进而使程序调用异常处置惩罚或信号处置惩罚函数来办理题目。
末了当hello程序执行结束后,子历程变为僵死历程,已调用waitpid函数父历程将子历程接纳,并删除干系内容。hello程序的一生就此结束。
在探索hello程序的一生的过程中,我深刻相识了一个程序从无到有再到无的一系列处置惩罚与过程,并借此对计算机系统的干系知识有了更深入的理解。包罗源程序的预处置惩罚、编译、汇编、链接,以及历程、异常与信号、存储管理、I/O管理等。计算机系统这门课让我学到了很多,认识到计算机世界在丰富多彩的同时也是高深莫测的,另有更多的知识等待着我学习,更多的奥秘等待着我探索。在以后的学习中,我也将抱持活泼的思考,多动手,不断增强自己的本事。
附件
文件名
| 作用
| hello.c
| hello的C源文件
| hello.i
| hello.c预处置惩罚后得到的预编译文本文件
| hello.s
| hello.i经过编译后得到的汇编语言文本文件
| hello.o
| hello.s经过汇编得到的可重定位目标文件
| hello
| hello.o经过链接得到的可执行目标文件
| hello_o.elf
| hello.o的ELF格式文件
| hello_o_asm.txt
| hello.o反汇编后的文件
| hello.elf
| hello的ELF格式文件
| hello_asm.txt
| hello反汇编后的文件
|
参考文献
[1] 《深入理解计算机系统》Randal E. Bryant David R.O`Hallaron
[2] https://www.cnblogs.com/pianist/p/3315801.html
[3] https://zhuanlan.zhihu.com/p/458932445
[4] https://blog.csdn.net/Nod_Mouse/article/details/114654965
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |