摘 要
本作业论文呈现了计算机系统课程大作业“程序人生-Hello’s P2P”的实验过程及所得结果。通过对hello的自述进行分析,随着它的自白一步步使用Linux系统下的gcc、gdb、objdump、edb工具经过预处理、编译、汇编、链接生成可执行文件,详细地观察它P2P每一步背后的经过及原理,并按要求得到相关结论。随后通过shell,使用fork()、exevce()等函数,对hello可执行文件进行进一步的分析,对其020的进程背后的内存分配、数据交互、I/O系统进行了观察,同样按照要求进行了相关工作与记录。在这样的过程中,陪伴hello走过了它的一生,深刻理解了hello从生成到可执行再到执行背后的计算机系统原理。
关键词:计算机原理;预处理;编译;汇编;链接;进程;存储管理;IO管理
目 录
第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的预处理结果解析 - 6 -
2.4 本章小结 - 7 -
第3章 编译 - 8 -
3.1 编译的概念与作用 - 8 -
3.2 在Ubuntu下编译的命令 - 8 -
3.3 Hello的编译结果解析 - 8 -
3.4 本章小结 - 10 -
第4章 汇编 - 11 -
4.1 汇编的概念与作用 - 11 -
4.2 在Ubuntu下汇编的命令 - 11 -
4.3 可重定位目标elf格式 - 11 -
4.4 Hello.o的结果解析 - 15 -
4.5 本章小结 - 16 -
第5章 链接 - 19 -
5.1 链接的概念与作用 - 19 -
5.2 在Ubuntu下链接的命令 - 19 -
5.3 可执行目标文件hello的格式 - 20 -
5.4 hello的虚拟地址空间 - 25 -
5.5 链接的重定位过程分析 - 26 -
5.6 hello的执行流程 - 31 -
5.7 Hello的动态链接分析 - 33 -
5.8 本章小结 - 33 -
第6章 hello进程管理 - 35 -
6.1 进程的概念与作用 - 35 -
6.2 简述壳Shell-bash的作用与处理流程 - 35 -
6.3 Hello的fork进程创建过程 - 35 -
6.4 Hello的execve过程 - 36 -
6.5 Hello的进程执行 - 37 -
6.6 hello的异常与信号处理 - 38 -
6.7本章小结 - 41 -
第7章 hello的存储管理 - 42 -
7.1 hello的存储器地址空间 - 42 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 42 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 45 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 46 -
7.5 三级Cache支持下的物理内存访问 - 47 -
7.6 hello进程fork时的内存映射 - 48 -
7.7 hello进程execve时的内存映射 - 48 -
7.8 缺页故障与缺页中断处理 - 49 -
7.9动态存储分配管理 - 50 -
7.10本章小结 - 53 -
第8章 hello的IO管理 - 54 -
8.1 Linux的IO设备管理方法 - 54 -
8.2 简述Unix IO接口及其函数 - 54 -
8.3 printf的实现分析 - 55 -
8.4 getchar的实现分析 - 56 -
8.5本章小结 - 57 -
结论 - 57 -
附件 - 58 -
参考文献 - 59 -
第1章 概述
1.1 Hello简介
From program to progress 即从程序到进程的过程。在这个Hello的诞生过程中,经历了预处理(gcc -E hello.c -o hello.i)生成.i文件、编译(gcc -S hello.i -o hello.s)生成.S汇编文本文件、汇编(gcc -c hello.s -o hello.o)生成.o可重定位二进制文件以及链接(gcc hello.o -o hello)形成可执行文件的四个步骤,完成了从源文件hello.c到可执行文件hello的生成。随后shell对其执行,其父进程会通过folk一个子进程。在这个子进程中,会通过execve系统启动加载器并将紫禁城内的虚拟内存删除,新创建的代码、数据、堆栈并初始化为可执行文件。通过_start地址跳转调用main函数使p2p进程完成。
From Zero-0 to Zero-0 即020,首先在程序执行前分配内存空间。在execve系统启动后,删除虚拟内存、新建代码数据堆栈、初始化可执行文件内容、_start地址跳转main函数,之后开始运行。运行期间,其数据交互涉及到磁盘、主存、Cache......访存、读写数据。在则个过程中,涉及到存储空间的使用、计算机内部的信号处理、操作系统的控制进程等等,使得系统资源能够被高效利用。最后由I/O系统的printf使得进程完成。
Hello的一生也就这样结束。
1.2 环境与工具
硬件环境:Inter(R) Core(TM) i9-9880H CPU @ 2.30GHz 2.30GHz
软件环境:Windows 10 64位;Ubuntu 20.04.4 64位
开发工具:gcc;gdb;objdump;edb
1.3 中间结果
文件名 作用
hello.i .c文件预处理后的文件
hello.s 经编译后含有汇编语言的文本文件
hello.o 经汇编后二进制重定位的目标文件
hello/hello.out 生成的可执行文件
hello.txt hello的反汇编代码存储至txt文件中
表1-1 中间结果
1.4 本章小结
这一章是hello.c一生的简短概述,此生虽短,却体现了计算机精妙的工作原理以及幕后工作。接下来将对hello的一生进行更加细致的讨论。
第2章 预处理
2.1 预处理的概念与作用
预处理又叫预编译,是完整编译过程中的第一个阶段,在正式的编译阶段之前进行。
预处理阶段将根据已放置在文件中的预处理指令来修改源文件的内容,将源代码中的预处理指令根据语义预先处理,并且进行一下清理、标记工作,然后将这些代码输出到一个 .i 文件中等待进一步操作。
在这个阶段,主要进行了:文件包含、添加行号和文件名标识、宏定义展开与处理、条件编译处理、清理注释内容、特殊控制处理这六项工作。
2.2在Ubuntu下预处理的命令
图2-1 在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
预处理得到的hello.i文件经记事本打开后可以查看其内容。
其1~7行进行了文件名标识,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
其13~57行进行了文件包含,从系统目录下查找和优先在当前目录查找所引用包含的文件内容。
其60~174行进行了定义的展开与变量的处理。
其接下来的内容便是对源代码的内容通过预处理指令,根据语义进行了替换。
在文件末尾对hello.c文件内容进行了存留。
(文件全部内容详见附件)
2.4 本章小结
预处理作为完整编译流程的第一个阶段,基本上完成的是一个“替代”工作。以生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。其所用指令是:gcc -E xxx.c -o xxx.i;产生的文件是.i文件。
第3章 编译
3.1 编译的概念与作用
编译过程是整个程序构建的核心部分,也是最复杂的部分之一,其工作就是把预处理完生成的 .i 文件进行一系列编译后产生相应的汇编代码文件,也就是 .s 文件。
在这个过程中,主要做了以下4个工作:词法分析、语法分析、语义分析、代码优化。
3.2 在Ubuntu下编译的命令
图3-1 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
为方便展示,hello.s的内容如下,解析由注释给出。
.file "hello.c"
.text
.section .rodata
.align 8
.LC0:
.string"\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"
#该句定义了printf中所要打印的字符串内容。
.LC1:
.string "Hello %s %s\n" #定义了循环内printf的字符串内容
.text #指定了后续编译出来的内容放在代码段可执行
.globl main #声明了全局可见变量main
.type main, @function #定义函数处理
main:
.LFB6:
.cfi_startproc
endbr64
pushq %rbp #将%rbp压入栈中
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp #在%rbp中存储下栈的基地址
.cfi_def_cfa_register 6
subq $32, %rsp #通过减法开拓栈空间
movl %edi, -20(%rbp) #将%edi所存的int argc送至距离基地址偏移20个字节的栈内
movq %rsi, -32(%rbp) #将%rsi所存的char *argv[]送至距离基地址偏移32个字节的栈内
cmpl $4, -20(%rbp) #if(argc!=4),如果argc不等于4
je .L2 #跳转到.L2
leaq .LC0(%rip), %rdi #否则继续,即将%rip内容传入.LC0并将返回值传送给%rdi。
call puts@PLT #调用进程
movl $1, %edi #将%edi的值赋为1
call exit@PLT #调用进程
.L2:
movl $0, -4(%rbp) #将0传送至偏移基地址4个字节的栈内,也即将i初始化为0
jmp .L3 #跳转至.L3
.L4:
movq -32(%rbp), %rax #将char*argv[]传送至%rax中
addq $16, %rax #对其加16得到argv[1]地址
movq (%rax), %rdx #将argv[1]的值送入%rdx
movq -32(%rbp), %rax #将char*argv[]传送至%rax中
addq $8, %rax #对其加8得到argv[2]地址
movq (%rax), %rax #将argv[2]的值送入%rax
movq %rax, %rsi #将其再送入%rsi
leaq .LC1(%rip), %rdi #将%rip内所存送至.LC1得到的返回值送回%rdi
movl $0, %eax #将%eax的值赋为0,也将整体清零
call printf@PLT #调用进程
movq -32(%rbp), %rax #将char*argv[]传送至%rax中
addq $24, %rax #将其加24得到argv[3]地址
movq (%rax), %rax #将argv[3]的值直接存入%rax中
movq %rax, %rdi #再转存到%rdi中
call atoi@PLT #调用进程
movl %eax, %edi #将%edi也清零
call sleep@PLT #调用进程
addl $1, -4(%rbp) #i++
.L3:
cmpl $7, -4(%rbp) #循环中对i的判断
jle .L4 #如果比8小则继续循环
call getchar@PLT #调用进程(getchar)
movl $0, %eax #将%eax内清零
leave
.cfi_def_cfa 7, 8
ret #返回值
.cfi_endproc
.LFE6:
.size main, .-main
.ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
(文件全部内容详见txt附件)
3.4 本章小结
编译过程是P2P过程中相当重要的一个环节,gcc在这个环节会检查代码的规范性与语法正确性,保证程序正确性的基础上,确定代码实际要做的工作,优化代码、并将代码翻译成汇编语言。从高级语言汇编语言的转换,为不同高级语言、不同编译器提供了通用的语言,使得程序的编译有了共同基础。通过对编译环节的研究、对.S文件内容的分析,我们也能更好的理解计算机的底层逻辑运作,使我们计算机思维得到锻炼。其命令是gcc -S xxx.i -o xxx.s
第4章 汇编
4.1 汇编的概念与作用
汇编过程是整个程序构建中的第三步,是将编译产生的汇编代码文件转变成可执行的机器指令。
在这个过程中,每个汇编语句都有相对应的机器指令,根据汇编代码语法和机器指令的对照表翻译,最终生成目标文件,也就是 .o 文件。
目标文件中所存放的也就是与源程序等效的目标的机器语言代码,通常至少包含代码段和数据段两个段,并且还要包含未解决符号表,导出符号表和地址重定向表等3个表。汇编过程会将extern声明的变量置入未解决符号表,将static声明的全局变量不置入未解决符号表,也不置入导出符号表,无法被其他目标文件使用,然后将普通变量及函数置入导出符号表,供其他目标文件使用。
4.2 在Ubuntu下汇编的命令
图4-1 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
1.ELF头,用命令readelf -h hello.o查看,结果如下图:
图4-2 查看ELF头
分析: 从ELF64可以看出hello.o为64为64位文件
从Class可以看出它以补码表示数据,小端存储
从Type可以看出它文件类型位REL,即可重定位目标文件
从Entry可以看出程序入口为0x0
从start of program header可以看出由于是可重定位文件,无段头表
从start of section header可以看出节头表起始位置为1240
由Number of section headers可以看出文件一共14节
2.节头表,用readelf -S hello.o查看,结果如下图:
图4-3-1 查看节头表
图4-3-2 查看节头表
分析:由上图结果可得,该文件共有由14项,每一项之后都可以得到该节的名字、类型、地址,但是由于未重定位,地址为0,以及文件中的偏移量、大小、访问权限、对齐方式等等内容。
3.符号表用readelf -s hello.o查看,结果如下
图4-4 查看符号表
分析:由上图结果可得,符号表内存储了程序中所定义的符号,如printf、getchar等函数,由每一行即可得到相应信息。
4.重定位节,用readelf -r hello.o查看,结果如下图:
图4-5 查看重定位节
分析: 由offset可以得到需要被重定位的节的偏移
由Info可以得到被修改的节所应该指向的符号(symbol)以及重定位的类型(type)
由Type(不同于上面的type)可得到。链接器应该如何修改。
由Addend可得到引用的值所做的偏移调整。
由Name可以得到重定位的目标名称
4.4 Hello.o的结果解析
通过objdump -d -r hello.o得到的结果如下,为方便分析,展示出所有内容并以注释形式给出分析:
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp #将%rbp压入栈中
5: 48 89 e5 mov %rsp,%rbp #存储栈的基地址
8: 48 83 ec 20 sub $0x20,%rsp #拓栈栈的内存
c: 89 7d ec mov %edi,-0x14(%rbp) #将%edi存储的内容,即int argv放入偏离基地址0x14的位置
f: 48 89 75 e0 mov %rsi,-0x20(%rbp) #将%rsi存储的内容,即char *argv[]放入偏离基地址0x20的位置
13: 83 7d ec 04 cmpl $0x4,-0x14(%rbp) #比较argv与4,完成if判断
17: 74 16 je 2f <main+0x2f> #若等于则跳转
19: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 20 <main+0x20>
1c: R_X86_64_PC32 .rodata-0x4
20: e8 00 00 00 00 callq 25 <main+0x25> #调用进程
21: R_X86_64_PLT32 puts-0x4
25: bf 01 00 00 00 mov $0x1,%edi #将1赋给%edi
2a: e8 00 00 00 00 callq 2f <main+0x2f> #调用进程
2b: R_X86_64_PLT32 exit-0x4
2f: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) #将i初始化为0
36: eb 48 jmp 80 <main+0x80> #无条件跳转
38: 48 8b 45 e0 mov -0x20(%rbp),%rax #将char *argv[]地址存入%rax
3c: 48 83 c0 10 add $0x10,%rax #将%rax内值加0X10,即取argv[1]地址
40: 48 8b 10 mov (%rax),%rdx #将argv[1]的值存入%rdx
43: 48 8b 45 e0 mov -0x20(%rbp),%rax #将char *argv[]地址存入%rax
47: 48 83 c0 08 add $0x8,%rax #将%rax内值加0X8,即取argv[2]地址
4b: 48 8b 00 mov (%rax),%rax #将argv[2]的值存入%rax
4e: 48 89 c6 mov %rax,%rsi #将该值赋给%rsi
51: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 58 <main+0x58>
54: R_X86_64_PC32 .rodata+0x22
58: b8 00 00 00 00 mov $0x0,%eax #%eax清零
5d: e8 00 00 00 00 callq 62 <main+0x62> #调用进程
5e: R_X86_64_PLT32 printf-0x4
62: 48 8b 45 e0 mov -0x20(%rbp),%rax #将char *argv[]地址存入%rax
66: 48 83 c0 18 add $0x18,%rax #将%rax内值加0X18,即取argv[3]地址
6a: 48 8b 00 mov (%rax),%rax #将argv[3]的值存入%rax
6d: 48 89 c7 mov %rax,%rdi #将该值存入%rdi
70: e8 00 00 00 00 callq 75 <main+0x75> #调用进程
71: R_X86_64_PLT32 atoi-0x4
75: 89 c7 mov %eax,%edi #将%eax内容存入%edi
77: e8 00 00 00 00 callq 7c <main+0x7c> #调用进程
78: R_X86_64_PLT32 sleep-0x4
7c: 83 45 fc 01 addl $0x1,-0x4(%rbp) #i++
80: 83 7d fc 07 cmpl $0x7,-0x4(%rbp) #判断i与7的关系
84: 7e b2 jle 38 <main+0x38> #若小于等于,则跳转继续循环
86: e8 00 00 00 00 callq 8b <main+0x8b> #调用进程
87: R_X86_64_PLT32 getchar-0x4
8b: b8 00 00 00 00 mov $0x0,%eax #%eax清零
90: c9 leaveq
91: c3 retq #返回值
通过和.s文件分析的对比,可以发现,在大体结构和功能实现上,两者相差不大,都能够反映良好逻辑与代码功能的实现。而两者对比之下也体现了机器语言和汇编语言的区别。
机器语言是一种指令集的体系。这种指令集也被叫做机械码,是电脑cpu能直接解读的数据。在用二进制代码去表示和执行操作,使用操作码字段和地址码字段来指明操作性质和操作地址。体现在分支转移函数更在于固定分配的进程地址以及偏移量的控制来控制调用。
而在汇编语言是面向机器的程序设计语言,用助记符代替操作码,用地址符号或者标号来代替二进制码,故而汇编语言也被称为符号语言。体现在分支转移函数中便在于标号对不同进程的划分以及直接使用。
4.5 本章小结
汇编步骤将编译产生的汇编代码文件转变为可执行的机器指令。其代码段半酣主要程序指令,可读可执行一般不可写;数据段存放全局变量和静态数据,一般可读可写可执行;未解决符号表列出了有引用单不存在定义的符号;导出符号表列出了本目标文件里具有定义,并且可以提供给其他目标文件使用的符号及其在出现的地址;地址重定向表: 列出了本目标文件里所有对自身地址的引用记录。汇编得到的机器码文件与汇编语言不同,更加“低级”但是能够反映正确逻辑并且被cpu接受。
通过redelf可对ELF格式的文件信息进行查看,其用法汇总如下:
-a
--all 显示全部信息,等价于 -h -l -S -s -r -d -V -A -I.
-h
--file-header 显示elf文件开始的文件头信息.
-l
--program-headers
--segments 显示程序头(段头)信息(如果有的话)。
-S
--section-headers
--sections 显示节头信息(如果有的话)。
-g
--section-groups 显示节组信息(如果有的话)。
-t
--section-details 显示节的详细信息(-S的)。
-s
--syms
--symbols 显示符号表段中的项(如果有的话)。
-e
--headers 显示全部头信息,等价于: -h -l -S
-n
--notes 显示note段(内核注释)的信息。
-r
--relocs 显示可重定位段的信息。
-u
--unwind 显示unwind段信息。当前只支持IA64 ELF的unwind段信息。
-d
--dynamic 显示动态段的信息。
-V
--version-info 显示版本段的信息。
-A
--arch-specific 显示CPU构架信息。
-D
--use-dynamic 使用动态段中的符号表显示符号,而不是使用符号段。
-x <number or name>
--hex-dump=<number or name> 以16进制方式显示指定段内内容。number指定段表中段的索引,或字符串指定文件中的段名。
-w[liaprmfFsoR] or
--debug-dump[=line,=info,=abbrev,=pubnames,=aranges,=macro,=frames,=frames-interp,=str,=loc,=Ranges] 显示调试段中指定的内容。
-I
--histogram 显示符号的时候,显示bucket list长度的柱状图。
-v
--version 显示readelf的版本信息。
-H
--help 显示readelf所支持的命令行选项。
-W
--wide 宽行输出。
反汇编的操作与分析能够让我们对机器语言的组成、汇编语言的映射关系有更好的理解与感知。
汇编步骤指令为:gcc -c xxx.s -o xxx.0
第5章 链接
5.1 链接的概念与作用
链接过程是程序构建过程的最后一步,该步骤将目标文件和库文件打包组装成可执行文件的过程,其主要内容就是把各个模块之间相互引用的部分都处理好,将一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得各个模块之间能够正确的衔接,成为一个能够被操作系统装入执行的统一整体。
链接包含动态链接和静态链接。
静态链接即函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时,代码将被装入到该进程的虚拟地址空间中,静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码,最终生成的可执行文件较大。
动态链接即函数的代码被放到动态链接库或共享对象的某个目标文件中。链接处理时只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在这样该程序在被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间,根据可执行程序中记录的信息找到相应的函数代码。这种连接方法能节约一定的内存,也可以减小生成的可执行文件体积。
5.2 在Ubuntu下链接的命令
图5-1 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
分析与hello.o的过程类似
1.用readelf -h hello查看ELF头,结果如下:
图5-2 查看ELF头
分析: 从ELF64可以看出hello.o为64为64位文件
从Class可以看出它以补码表示数据,小端存储
从Type可以看出它文件类型位EXEC,即可执行目标文件
从Entry可以看出程序入口为0x4010f0
从start of program header可以看出段头表起始位置是64
从start of section header可以看出节头表起始位置为1240
由Number of section headers可以看出文件一共30节
2.用readelf -S hello查看节头表,结果如下:
图5-2-1 查看节头表
图5-2-2 查看节头表
图5-2-3 查看节头表
分析:由上图结果可得,共有由29项,条目数增加了。每一项之后都可以得到该节的名字、类型、地址,而由于重定位,地址已经不再是0,以及文件中的偏移量、大小、访问权限、对齐方式等等内容。多出的节实现了更多链接功能。
3.用readelf -s hello查看符号表,结果如下:
图5-3-1 查看符号表
图5-3-2 查看符号表
图5-3-3 查看符号表
分析:由上图可看出,一共两个符号表,各符号信息详细如图。
4.用readelf -l hello查看段头表,结果如下:
图5-4-1 查看段头表
图5-4-2 查看段头表
分析:由上图可知,一共12个程序头,可以看到各自的分配状况,整体一共两段,分别为只读内存段即代码段,还有读写代码段即数据段。
5.4 hello的虚拟地址空间
在上一步中,我们已经查看了各部分的起始地址,例如ELF头中得到的程序入口地址为0x4010f0(再进一步查看发现是.text部分),在edb中查看可得:
图5-5-1 edb查看截图
再如节头表中的.interp节的起始地址为0x4002e0,在edb中查看可得这里正是存放着连接器的路径名:
图5-5-2 edb查看截图
而根据节头表的各个节的位置可以定位到任意节的位置。如.data节在0x404000位置,在edb中查看可得:
图5-5-3 edb查看截图
其余各节同理,不再赘述。
5.5 链接的重定位过程分析
图5-6 生成过程截图
通过objdump -d -r hello >hello.txt将hello文件的反汇编代码生成到hello.txt文
件中。
下面给出结果,并且给出部分分析:
hello: file format elf64-x86-64
Disassembly of section .init: #程序的初始化调用.init
0000000000401000 <_init>:
401000: f3 0f 1e fa endbr64
401004: 48 83 ec 08 sub $0x8,%rsp
401008: 48 8b 05 e9 2f 00 00 mov 0x2fe9(%rip),%rax # 403ff8 <__gmon_start__>
40100f: 48 85 c0 test %rax,%rax
401012: 74 02 je 401016 <_init+0x16>
401014: ff d0 callq *%rax
401016: 48 83 c4 08 add $0x8,%rsp
40101a: c3 retq
Disassembly of section .plt:
0000000000401020 <.plt>: #程序的动态链接调用.plt
401020: ff 35 e2 2f 00 00 pushq 0x2fe2(%rip) # 404008 <_GLOBAL_OFFSET_TABLE_+0x8>
401026: f2 ff 25 e3 2f 00 00 bnd jmpq *0x2fe3(%rip) # 404010 <_GLOBAL_OFFSET_TABLE_+0x10>
40102d: 0f 1f 00 nopl (%rax)
401030: f3 0f 1e fa endbr64
401034: 68 00 00 00 00 pushq $0x0
401039: f2 e9 e1 ff ff ff bnd jmpq 401020 <.plt>
40103f: 90 nop
401040: f3 0f 1e fa endbr64
401044: 68 01 00 00 00 pushq $0x1
401049: f2 e9 d1 ff ff ff bnd jmpq 401020 <.plt>
40104f: 90 nop
401050: f3 0f 1e fa endbr64
401054: 68 02 00 00 00 pushq $0x2
401059: f2 e9 c1 ff ff ff bnd jmpq 401020 <.plt>
40105f: 90 nop
401060: f3 0f 1e fa endbr64
401064: 68 03 00 00 00 pushq $0x3
401069: f2 e9 b1 ff ff ff bnd jmpq 401020 <.plt>
40106f: 90 nop
401070: f3 0f 1e fa endbr64
401074: 68 04 00 00 00 pushq $0x4
401079: f2 e9 a1 ff ff ff bnd jmpq 401020 <.plt>
40107f: 90 nop
401080: f3 0f 1e fa endbr64
401084: 68 05 00 00 00 pushq $0x5
401089: f2 e9 91 ff ff ff bnd jmpq 401020 <.plt>
40108f: 90 nop
Disassembly of section .plt.sec:
0000000000401090 <puts@plt>:
401090: f3 0f 1e fa endbr64
401094: f2 ff 25 7d 2f 00 00 bnd jmpq *0x2f7d(%rip) # 404018 <puts@GLIBC_2.2.5>
40109b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000004010a0 <printf@plt>:
4010a0: f3 0f 1e fa endbr64
4010a4: f2 ff 25 75 2f 00 00 bnd jmpq *0x2f75(%rip) # 404020 <printf@GLIBC_2.2.5>
4010ab: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000004010b0 <getchar@plt>:
4010b0: f3 0f 1e fa endbr64
4010b4: f2 ff 25 6d 2f 00 00 bnd jmpq *0x2f6d(%rip) # 404028 <getchar@GLIBC_2.2.5>
4010bb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000004010c0 <atoi@plt>:
4010c0: f3 0f 1e fa endbr64
4010c4: f2 ff 25 65 2f 00 00 bnd jmpq *0x2f65(%rip) # 404030 <atoi@GLIBC_2.2.5>
4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000004010d0 <exit@plt>:
4010d0: f3 0f 1e fa endbr64
4010d4: f2 ff 25 5d 2f 00 00 bnd jmpq *0x2f5d(%rip) # 404038 <exit@GLIBC_2.2.5>
4010db: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000004010e0 <sleep@plt>:
4010e0: f3 0f 1e fa endbr64
4010e4: f2 ff 25 55 2f 00 00 bnd jmpq *0x2f55(%rip) # 404040 <sleep@GLIBC_2.2.5>
4010eb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
Disassembly of section .text:
00000000004010f0 <_start>:
4010f0: f3 0f 1e fa endbr64
4010f4: 31 ed xor %ebp,%ebp
4010f6: 49 89 d1 mov %rdx,%r9
4010f9: 5e pop %rsi
4010fa: 48 89 e2 mov %rsp,%rdx
4010fd: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
401101: 50 push %rax
401102: 54 push %rsp
401103: 49 c7 c0 e0 12 40 00 mov $0x4012e0,%r8
40110a: 48 c7 c1 70 12 40 00 mov $0x401270,%rcx
401111: 48 c7 c7 d6 11 40 00 mov $0x4011d6,%rdi
401118: ff 15 d2 2e 00 00 callq *0x2ed2(%rip) # 403ff0 <__libc_start_main@GLIBC_2.2.5>
40111e: f4 hlt
40111f: 90 nop
0000000000401120 <_dl_relocate_static_pie>:
401120: f3 0f 1e fa endbr64
401124: c3 retq
401125: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
40112c: 00 00 00
40112f: 90 nop
0000000000401130 <deregister_tm_clones>:
401130: b8 58 40 40 00 mov $0x404058,%eax
401135: 48 3d 58 40 40 00 cmp $0x404058,%rax
40113b: 74 13 je 401150 <deregister_tm_clones+0x20>
40113d: b8 00 00 00 00 mov $0x0,%eax
401142: 48 85 c0 test %rax,%rax
401145: 74 09 je 401150 <deregister_tm_clones+0x20>
401147: bf 58 40 40 00 mov $0x404058,%edi
40114c: ff e0 jmpq *%rax
40114e: 66 90 xchg %ax,%ax
401150: c3 retq
401151: 66 66 2e 0f 1f 84 00 data16 nopw %cs:0x0(%rax,%rax,1)
401158: 00 00 00 00
40115c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000401160 <register_tm_clones>:
401160: be 58 40 40 00 mov $0x404058,%esi
401165: 48 81 ee 58 40 40 00 sub $0x404058,%rsi
40116c: 48 89 f0 mov %rsi,%rax
40116f: 48 c1 ee 3f shr $0x3f,%rsi
401173: 48 c1 f8 03 sar $0x3,%rax
401177: 48 01 c6 add %rax,%rsi
40117a: 48 d1 fe sar %rsi
40117d: 74 11 je 401190 <register_tm_clones+0x30>
40117f: b8 00 00 00 00 mov $0x0,%eax
401184: 48 85 c0 test %rax,%rax
401187: 74 07 je 401190 <register_tm_clones+0x30>
401189: bf 58 40 40 00 mov $0x404058,%edi
40118e: ff e0 jmpq *%rax
401190: c3 retq
401191: 66 66 2e 0f 1f 84 00 data16 nopw %cs:0x0(%rax,%rax,1)
401198: 00 00 00 00
40119c: 0f 1f 40 00 nopl 0x0(%rax)
00000000004011a0 <__do_global_dtors_aux>:
4011a0: f3 0f 1e fa endbr64
4011a4: 80 3d ad 2e 00 00 00 cmpb $0x0,0x2ead(%rip) # 404058 <__TMC_END__>
4011ab: 75 13 jne 4011c0 <__do_global_dtors_aux+0x20>
4011ad: 55 push %rbp
4011ae: 48 89 e5 mov %rsp,%rbp
4011b1: e8 7a ff ff ff callq 401130 <deregister_tm_clones>
4011b6: c6 05 9b 2e 00 00 01 movb $0x1,0x2e9b(%rip) # 404058 <__TMC_END__>
4011bd: 5d pop %rbp
4011be: c3 retq
4011bf: 90 nop
4011c0: c3 retq
4011c1: 66 66 2e 0f 1f 84 00 data16 nopw %cs:0x0(%rax,%rax,1)
4011c8: 00 00 00 00
4011cc: 0f 1f 40 00 nopl 0x0(%rax)
00000000004011d0 <frame_dummy>:
4011d0: f3 0f 1e fa endbr64
4011d4: eb 8a jmp 401160 <register_tm_clones>
00000000004011d6 <main>: #在hello.o的反汇编文件中,开始便是main #并且此处所有的符号赋予了确定的地址值
4011d6: f3 0f 1e fa endbr64
4011da: 55 push %rbp
4011db: 48 89 e5 mov %rsp,%rbp
4011de: 48 83 ec 20 sub $0x20,%rsp
4011e2: 89 7d ec mov %edi,-0x14(%rbp)
4011e5: 48 89 75 e0 mov %rsi,-0x20(%rbp)
4011e9: 83 7d ec 04 cmpl $0x4,-0x14(%rbp)
4011ed: 74 16 je 401205 <main+0x2f>
4011ef: 48 8d 3d 12 0e 00 00 lea 0xe12(%rip),%rdi # 402008 <_IO_stdin_used+0x8>
4011f6: e8 95 fe ff ff callq 401090 <puts@plt>
4011fb: bf 01 00 00 00 mov $0x1,%edi
401200: e8 cb fe ff ff callq 4010d0 <exit@plt>
401205: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
40120c: eb 48 jmp 401256 <main+0x80>
40120e: 48 8b 45 e0 mov -0x20(%rbp),%rax
401212: 48 83 c0 10 add $0x10,%rax
401216: 48 8b 10 mov (%rax),%rdx
401219: 48 8b 45 e0 mov -0x20(%rbp),%rax
40121d: 48 83 c0 08 add $0x8,%rax
401221: 48 8b 00 mov (%rax),%rax
401224: 48 89 c6 mov %rax,%rsi
401227: 48 8d 3d 00 0e 00 00 lea 0xe00(%rip),%rdi # 40202e <_IO_stdin_used+0x2e>
40122e: b8 00 00 00 00 mov $0x0,%eax
401233: e8 68 fe ff ff callq 4010a0 <printf@plt>
401238: 48 8b 45 e0 mov -0x20(%rbp),%rax
40123c: 48 83 c0 18 add $0x18,%rax
401240: 48 8b 00 mov (%rax),%rax
401243: 48 89 c7 mov %rax,%rdi
401246: e8 75 fe ff ff callq 4010c0 <atoi@plt>
40124b: 89 c7 mov %eax,%edi
40124d: e8 8e fe ff ff callq 4010e0 <sleep@plt>
401252: 83 45 fc 01 addl $0x1,-0x4(%rbp)
401256: 83 7d fc 07 cmpl $0x7,-0x4(%rbp)
40125a: 7e b2 jle 40120e <main+0x38>
40125c: e8 4f fe ff ff callq 4010b0 <getchar@plt>
401261: b8 00 00 00 00 mov $0x0,%eax
401266: c9 leaveq
401267: c3 retq
401268: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
40126f: 00
0000000000401270 <__libc_csu_init>:
401270: f3 0f 1e fa endbr64
401274: 41 57 push %r15
401276: 4c 8d 3d 83 2b 00 00 lea 0x2b83(%rip),%r15 # 403e00 <__frame_dummy_init_array_entry>
40127d: 41 56 push %r14
40127f: 49 89 d6 mov %rdx,%r14
401282: 41 55 push %r13
401284: 49 89 f5 mov %rsi,%r13
401287: 41 54 push %r12
401289: 41 89 fc mov %edi,%r12d
40128c: 55 push %rbp
40128d: 48 8d 2d 74 2b 00 00 lea 0x2b74(%rip),%rbp # 403e08 <__do_global_dtors_aux_fini_array_entry>
401294: 53 push %rbx
401295: 4c 29 fd sub %r15,%rbp
401298: 48 83 ec 08 sub $0x8,%rsp
40129c: e8 5f fd ff ff callq 401000 <_init>
4012a1: 48 c1 fd 03 sar $0x3,%rbp
4012a5: 74 1f je 4012c6 <__libc_csu_init+0x56>
4012a7: 31 db xor %ebx,%ebx
4012a9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
4012b0: 4c 89 f2 mov %r14,%rdx
4012b3: 4c 89 ee mov %r13,%rsi
4012b6: 44 89 e7 mov %r12d,%edi
4012b9: 41 ff 14 df callq *(%r15,%rbx,8)
4012bd: 48 83 c3 01 add $0x1,%rbx
4012c1: 48 39 dd cmp %rbx,%rbp
4012c4: 75 ea jne 4012b0 <__libc_csu_init+0x40>
4012c6: 48 83 c4 08 add $0x8,%rsp
4012ca: 5b pop %rbx
4012cb: 5d pop %rbp
4012cc: 41 5c pop %r12
4012ce: 41 5d pop %r13
4012d0: 41 5e pop %r14
4012d2: 41 5f pop %r15
4012d4: c3 retq
4012d5: 66 66 2e 0f 1f 84 00 data16 nopw %cs:0x0(%rax,%rax,1)
4012dc: 00 00 00 00
00000000004012e0 <__libc_csu_fini>:
4012e0: f3 0f 1e fa endbr64
4012e4: c3 retq
Disassembly of section .fini:
00000000004012e8 <_fini>:
4012e8: f3 0f 1e fa endbr64
4012ec: 48 83 ec 08 sub $0x8,%rsp
4012f0: 48 83 c4 08 add $0x8,%rsp
4012f4: c3 retq
hello.o的反汇编结果在上一章已经给出分析展示,不再展示。下面分析两者不同。第一处很大的不同就是长度,hello.o的反汇编文件长度明显比hello的要短,原因是hello中除了hello的主函数的汇编代码,由于其进行了链接的重定位,各部分函数的引用被确定,还包含了许多其他函数的汇编代码,如getchar等等。再就是由于重定位使得各节各符号的地址得到确定,在反汇编代码的每一行开始处,都不再是从0开始,而是如我们上文分析的一样从0x401000开始.
所以链接的过程简单来说就是对符号的地址不确定引用给予一个确定的地址,并且在引用时修改。
而其重定位过程,是指编译时,由于数据和代码的具体内存位置不知道,需要在引用时生成一个重定位的目标,以便于让链接器再将目标文件转换为可执行文件时知道如何引用。其具体过程是对重定位条目进行解读,之后采用PC相对寻址的重定位操作进行重定位,具体函数如下:
图5-7 重定位方法
在我们的hello中,可以通过查看hello.o的反汇编代码中如下段:
19: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 20 <main+0x20>
1c: R_X86_64_PC32 .rodata-0x4
这个式子就是PC相对寻址,我们需要知道在生成hello后,main函数的地址以及.rodata的地址,故查看hello的ELF文件内容,查询main函数地址为0x4011d6,.rodata的地址为0x402008,addend等于-4,Offset等于0x1c将数据带入PC相对寻址的运算公式:
*refer = (unsigned)(ADDR(.rodata)+addend-ADDR(main)-Offset)
=(unsigned)(0x402008+(-4)-(0x4011d6-0x1c)
=(unsigned)(0xe12)
我们接着查看hello的反汇编代码,得到:
4011ef: 48 8d 3d 12 0e 00 00 lea 0xe12(%rip),%rdi # 402008 <_IO_stdin_used+0x8>
发现结果是正确的。所以以上就是本程序重链接的过程。
5.6 hello的执行流程
由edb的analysis分析得到如下结果,adding后即为所进行程序及地址
DEBUG [Analyzer] adding: ld-2.31.so!_dl_catch_exception@plt <0x00007f790c632010>
DEBUG [Analyzer] adding: ld-2.31.so!malloc@plt <0x00007f790c632020>
DEBUG [Analyzer] adding: ld-2.31.so!_dl_signal_exception@plt <0x00007f790c632030>
DEBUG [Analyzer] adding: ld-2.31.so!calloc@plt <0x00007f790c632040>
DEBUG [Analyzer] adding: ld-2.31.so!realloc@plt <0x00007f790c632050>
DEBUG [Analyzer] adding: ld-2.31.so!_dl_signal_error@plt <0x00007f790c632060>
DEBUG [Analyzer] adding: ld-2.31.so!_dl_catch_error@plt <0x00007f790c632070>
DEBUG [Analyzer] adding: ld-2.31.so!_dl_rtld_di_serinfo <0x00007f790c63c090>
DEBUG [Analyzer] adding: ld-2.31.so!_dl_debug_state <0x00007f790c6431d0>
DEBUG [Analyzer] adding: ld-2.31.so!_dl_mcount <0x00007f790c644e00>
DEBUG [Analyzer] adding: ld-2.31.so!_dl_get_tls_static_info <0x00007f790c645680>
DEBUG [Analyzer] adding: ld-2.31.so!_dl_allocate_tls_init <0x00007f790c645770>
DEBUG [Analyzer] adding: ld-2.31.so!_dl_allocate_tls <0x00007f790c6459a0>
DEBUG [Analyzer] adding: ld-2.31.so!_dl_deallocate_tls <0x00007f790c645a10>
DEBUG [Analyzer] adding: ld-2.31.so!_dl_make_stack_executable <0x00007f790c646130>
DEBUG [Analyzer] adding: ld-2.31.so!_dl_find_dso_for_object <0x00007f790c646480>
DEBUG [Analyzer] adding: ld-2.31.so!_dl_exception_create <0x00007f790c649ca0>
DEBUG [Analyzer] adding: ld-2.31.so!_dl_exception_create_format <0x00007f790c649da0>
DEBUG [Analyzer] adding: ld-2.31.so!_dl_exception_free <0x00007f790c64a250>
DEBUG [Analyzer] adding: ld-2.31.so!__tunable_get_val <0x00007f790c64b5d0>
DEBUG [Analyzer] adding: ld-2.31.so!__tls_get_addr <0x00007f790c64bda0>
DEBUG [Analyzer] adding: ld-2.31.so!__get_cpu_features <0x00007f790c64bdf0>
DEBUG [Analyzer] adding: ld-2.31.so!malloc <0x00007f790c64e490>
DEBUG [Analyzer] adding: ld-2.31.so!calloc <0x00007f790c64e5b0>
DEBUG [Analyzer] adding: ld-2.31.so!free <0x00007f790c64e5f0>
DEBUG [Analyzer] adding: ld-2.31.so!realloc <0x00007f790c64e7e0>
DEBUG [Analyzer] adding: ld-2.31.so!_dl_signal_exception <0x00007f790c64ea70>
DEBUG [Analyzer] adding: ld-2.31.so!_dl_signal_error <0x00007f790c64eac0>
DEBUG [Analyzer] adding: ld-2.31.so!_dl_catch_exception <0x00007f790c64ec40>
DEBUG [Analyzer] adding: ld-2.31.so!_dl_catch_error <0x00007f790c64ed30>
DEBUG [Analyzer] adding: hello!_init <0x0000000000401000>
DEBUG [Analyzer] adding: hello!puts@plt <0x0000000000401030>
DEBUG [Analyzer] adding: hello!strtol@plt <0x0000000000401040>
DEBUG [Analyzer] adding: hello!__printf_chk@plt <0x0000000000401050>
DEBUG [Analyzer] adding: hello!exit@plt <0x0000000000401060>
DEBUG [Analyzer] adding: hello!sleep@plt <0x0000000000401070>
DEBUG [Analyzer] adding: hello!getc@plt <0x0000000000401080>
DEBUG [Analyzer] adding: hello!_start <0x00000000004010f0>
DEBUG [Analyzer] adding: hello!_dl_relocate_static_pie <0x0000000000401120>
DEBUG [Analyzer] adding: hello!deregister_tm_clones <0x0000000000401130>
DEBUG [Analyzer] adding: hello!register_tm_clones <0x0000000000401160>
DEBUG [Analyzer] adding: hello!__do_global_dtors_aux <0x00000000004011a0>
DEBUG [Analyzer] adding: hello!frame_dummy <0x00000000004011d0>
DEBUG [Analyzer] adding: hello!main <0x00000000004011d6>
DEBUG [Analyzer] adding: hello!__libc_csu_init <0x0000000000401260>
DEBUG [Analyzer] adding: hello!__libc_csu_fini <0x00000000004012d0>
DEBUG [Analyzer] adding: hello!_fini <0x00000000004012d8>
5.7 Hello的动态链接分析
首先查看hello的ELF文件,有下图结果:
图5-8-1 查看结果截图
所以找到0x404000地址,在初始化前可以看到下图结果:
图5-8-2 查看结果截图
而在初始化之后得到下图结果:
图5-8-2 查看结果截图
5.8 本章小结
链接的的工作就是把一些指令对其他符号地址的引用加以修正,主要包括了地址和空间分配、符号决议和重定位等步骤,而主要分为符号解析和重定位两步。符号解析步骤中,链接器将每个符号引用与一个确定的符号定义关联起来;重定位步骤中,将多个代码节和书局街合并为单个节,将符号从它们的在.o文件的相对位置重新定位到可执行文件中的最终绝对内存位置,并更新所有对这些富豪的引用来反映它们的新位置。根据开发人员指定的链接库函数的方式不同,链接过程可分为静态链接和动态链接两种,链接静态的库,需要拷贝到一起,链接动态的库需要登记一下库的信息。经过此步骤后便生成了可执行文件。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
作用:其作用在于使得程序看起来是独占使用处理器和内存、处理器也是无间断地执行我们程序中的指令,使得程序中的代码和数据表现为系统内存的唯一对象。清晰地刻画动态系统的内在规律,帮助我们有效管理和调度进入计算机系统主存储器运行的程序。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell是一个命令行解释器,是一个交互级应用程序,为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序,用户可以用shell来启动、挂起、停止甚至是编写一些程序。Shell还是一个功能相当强大的编程语言,易编写,易调试,灵活性较强。Shell是解释执行的脚本语言,在shell中可以直接调用Linux命令。
处理流程:1.读取命令行并分析命令行字符串与字符串参数,并传递给execve的argv项链。2.检查命令行参数是不是一个内置shell命令,如果不是,使用fork,然后再子进程中使用参数调用execve。3.对于不要求后台运行的命令,shell使用waitpid等待进程终止后返回,否则shell返回。(看命令行末尾的&)
6.3 Hello的fork进程创建过程
一个现有进程创建一个新进程需要调用fork函数,这个新进程被称为子进程。fork函数被调用一次但返回两次。两次返回的区别在于子进程中返回0值,而父进程中返回子进程的ID。
子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。同时因为子进程仅仅获得上述存储空间的"副本",故两者进程间不会共享这些存储空间。
UNIX将复制父进程的地址空间内容给子进程,使得子进程有了独立的地址空间。在不同的UNIX 系统下,fork之后是子进程先运行还是父进程先运行,依赖于系统的实现。
图6-1 fork的流程图举例
6.4 Hello的execve过程
1.fork被调用后,生成子进程。
2.子进程调用execve将新程序加载到子进程的内部空间,并将子进程的栈、数据等内容更新(除了进程ID,其余方面与原进程都不同)
3.根据不同的execve参数,execve调用启动加载器的操作系统代码来对对hello(fork出来的子进程)进行执行,包含两个方面,一个是加载运行,一个是execve参数传递。
4.加载运行方面,主要经过了:删除已存在的用户区域、映射私有区域、映射共享区域、设置程序计数器PC这几步。在参数方面:filename:包含准备载入当前进程空间的新程序的路径名。既可以是绝对路径,又可以是相对路径。argv[]:指定了传给新进程的命令行参数,该数组对应于c语言main函数的argv参数数组,格式也相同,argv[0]对应命令名,通常情况下该值与filename中的basename(就是绝对路径的最后一个)相同。envp[]:最后一个参数envp指定了新程序的环境列表。参数envp对应于新程序的environ数组。
图6-2 加载器是如何映射用户地址空间的区域的
6.5 Hello的进程执行
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
用户态和内核态:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
上下文切换:当一个进程正在执行时,内核调度了另一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。在进行上下文切换时,需要保存以前进程的上下文,恢复新恢复进程被保存的上下文,将控制传递给这个新恢复的进程来完成上下文切换。
图6-3 上下文切换过程图
Hello的过程执行:在用户模式下初始运行,在调用sleep之后进入内核模式,内核休眠请求主动释放当前进程并加载新的进程执行。定时器计时,内核进行上下文切换并执行其他进程。当计时结束,内核中断,中断信号被接受,hello进程重新从等待队列进入运行队列,继续自己的控制逻辑流。在调用getchar后,执行stdin的输入流,调用read返回,并且再次从用户模式进入内核模式。内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且完成从键盘处理器到内存的传输后,中断处理器。此后内核执行上下文切换,开始其他进程。当中断信号再次收到后,内核再次切回hello进程。
6.6 hello的异常与信号处理
类型 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复错误 同步 可能返回到当前指令
终止 不可恢复的错误 同步 不会返回
表6-1 异常的类型
hello执行过程中以上几种都可能出现。对于异常的处理,有以下三种情况:
1.处理程序将控制返回给当前指令,即当事件发生时正在执行的指令。
2.处理程序将控制返回给当前指令的下一条指令,即如果没有异常发生将会执行的下一条指令。
3.处理程序终止被中断的程序
下面根据不同操作进行异常与信号处理的分析:
1.不停乱按,包括回车
乱按键盘后,键入的内容会直接显示在控制台上。如果有回车的话,shell会把之前的字符串当作输入命令读入,如下图最后一行的,读了w。
图6-4-1 乱按及回车结果截图
2.Ctrl-z
按下ctrl-z之后,会停止前台作业,原因是发送了一个SIGTSTIP信号给前台进程组的每个进程。进程被挂起,若再发送SIFCONT则会继续执行进程。
图 6-4-2 ctrl-z结果截图
3.Ctrl-c
按ctrl-c后,前台进程终止,因为发送了一个SIGINT信号给前台进程组的每个进程。
图6-4-3 ctrl-c结果截图
4.Ctrl-z后运行ps、jobs、prtree、fg、kill等命令
Ctrl-Z后输入ps、jobs等命令仍会正常工作,并且此时hello程序的状态为Stopped。
ps和jpbs展示如图,fg的功能是使第一个后台作业变为前台,此处第一个后台作业是hello,所以输入fg 后hello程序又在前台开始运行。
图6-4-4 ctrl-z后输入ps,jpbs,fg结果截图
而输入kill会杀死进程。
图6-4-5 ctrl-z后输入kill结果截图
6.7本章小结
本章介绍了进程管理阶段的相关内容,如进程的概念与作用,还有Shell的处理过程与作用。对fork和execve函数的使用与过程进行了分析。对“hello先生”进程的执行过程“折腾了一番”,观察到了进程的执行中不同异常的状况及信号处理情况。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址是指在计算机体系结构中是指应用程序角度看到的内存单元(memory cell)、存储单元(storage element)、网络主机(network host)的地址。 逻辑地址往往不同于物理地址(physical address),通过地址翻译器(address translator)或映射函数可以把逻辑地址转化为物理地址。主要表现在机器语言指令中,用来制定一个操作数或者一条指令的地址,由一个段标识符加上一个指定段内相对地址的偏移量表示为“[段标识符:段内漂移量]”。
线性地址:线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。其实也就是虚拟地址,对应了硬件页式内存的转换前地址
虚拟地址:虚拟地址是Windows程序时运行在386保护模式下,程序访问存储器所使用的逻辑地址称为虚拟地址,与实地址模式下的分段地址类似,虚拟地址也可以写为“段:偏移量”的形式,这里的段是指段选择器。它对内存进行抽象描述,且不像物理内存真的对应物理地址中的地址元素,而是由操作系统协助相关硬件将虚拟地址转换成物理地址,即虚拟寻址过程。
物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。其把内存看作一个从0开始的数组,将数组作为物理地址,该数组的抽象过程及提供的寻址方式与地址总线相对应。
7.2 Intel逻辑地址到线性地址的变换-段式管理
从8086CPU开始,为了让程序在内存中能自由浮动而又不影响它的正常执行,CPU将内存划分成逻辑上的段来给程序使用。x86继续沿用了这一模式,但是用保护模式将其管理起来,进行保护。而段式管理正是用来对段进行管理的。
段式管理(segmentation),是指把一个程序分成若干个段(segment)进行存储,每个段都是一个逻辑实体(logical entity),程序员需要知道并使用它。它的产生是与程序的模块化直接有关的。段式管理是通过段表进行的,它包括段号或段名、段起点、装入位、段的长度等。此外还需要主存占用区域表、主存可用区域表。
段式管理主要包含两个部分,一个是内存管理,一个是保护措施。内存管理方面,为地址的转换提供基础平台。CPU在通过地址访问内存的时候会自动通过地址转换的基础平台进行转换。在段式内存管理中,分段机制的内存管理主要是提供从逻辑地址(logical address)转化为CPU的线性地址(Linear address)的基础平台。保护措施方面,控制访问行为,避免资源被随意访问。
其中几个重要的描述概念有:
1.段选择子,它是一个段的标识符,由16位字长的字段组成。低2位为RPL,表示请求者所使用的权限级别;第3位为描述索引表,0代表GDT(全局描述符表),1代表LDT(局部描述符表)。剩余位表示描述符表中的偏移地址,其本质是按照数组的寻址方式。
图7-1-1 段标识符
2.全局段描述符表。上面讲到Index中按照数组的寻址方式,其对应的数组其实就是段描述符表。在x86中有三类描述符表,GDT(Global Descriptor Table),LDT(Local Descriptor Table)和IDT(Interrupt Descriptor Table)。
这些段描述符表由段描述符寄存器(Descriptor Table Register)进行定位,分别是 GDTR,LDTR和IDTR。
图7-1-2 段描述符表
3.段描述符:段描述符表内存储的一个个元素被称为段描述符,它们被索引对应后能够描述一个具体的段。每个段描述符的长度是8字节,含三个主要字段:段基地址、段限长、段属性。段描述符通常由编译器、链接器、加载器或者操作系统来创建,但绝不是应用程序。其一般格式为下图:
图7-1-3 段标识符
在这些基础概念定义之上,便可以描述段式管理下,逻辑地址到线性地址的转换:首先得到逻辑地址[段选择符:段内偏移地址],查看段选择符内参数TI,若是0则对应GDT内段,是1则对应LDT内段。根据寄存器的地址与大小,得到数组。然后看段选择符的前13位,对段描述符表内的段描述进行具体寻址,根据段描述得到基地址,最后加上段内偏移地址,就可以得到线性地址。
图7-1-4 过程流程示意图
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是一种内存空间存储管理的技术,也是管理分为静态页式管理和动态页式管理,系统将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
线性地址到物理地址的变换过程与段式管理中逻辑地址到线性地址的构成类似,都是对地址描述进行取值,最后定位到寄存器中内容来定位某个管理条目,根据得到数据不同取不同段/页,然后对应得到基值,与偏移量相加得到新地址。在这里,虚拟地址(VA)被分为虚拟页号(VPN)和虚拟页偏移量(VPO),CPU取出虚拟页号,通过页表基地址寄存器来定位页表条目,在有效位为1时,从页表条目中取出信息物理页号(PPN),将PPN与虚拟页偏移量(PPN)组合得到物理地址。
图7-2 页式管理流程示意
7.4 TLB与四级页表支持下的VA到PA的变换
每当CPU产生一个虚拟地址,MMU(内存管理单元)就会查阅一个PTE(页表头目)以便于将虚拟地址翻译为物理地址。在最糟糕的情况下,这会要求内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就下降到1~2个周期。然而许多系统都试图消除这种开销,它们在MMU中包括了一个关于PTE的小的缓存,被称为翻译后备缓冲器,即TLB。
TLB是一个小的、虚拟地址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB具有高的相联度。
图7-3-1 虚拟地址中用以访问TLB的组成部分
用来压缩页表的常用方法是使用层次结构的页表,即多级页表。通过将虚拟地址的虚拟页号分为相同大小的不同部分,每个部分用于寻找上一级确定的页表基地址对应的页表条目,在逐页寻找过程中,找不到就缺页,进而访问下一级存储中的也,直至找到,然后将页写入上一级的快速缓存,循环往复。
具体的,在四级页表支持下,从VA到PA,首先VA通过VPN找到TLB,如果找到则得到了PPN物理页的信息,若不命中,则进入页项寻找,将VPN划分为9位1份的四份(共计36),然后对各页分别查找,找到下一级页表的项,最后在第四页中找到PPN。
图7-3-2 Core i7下四级页表翻译流程
多级页表在两个方面上减少了内存需求。一个是如果一级页表中的一个PTE为空,那么对应二级页表也就不会存在。另一个是只有一级页表才需要出现在主存中,虚拟内存系统可以在需要时创建、调入与调出二级页表,这就减少了主存的压力,只有最常用的二级页表才需要缓存在主存中。
图7-3-3 使用k级页表的地址翻译示意图
7.5 三级Cache支持下的物理内存访问
图7-4 Core i7地址翻译概况
如上图展示的Core i7地址翻译的概况所示,在翻译过程中,当我们得到了从TLB或者页表中得到了物理地址PA之后,会查看CT、CI、CO并进入L1寻址。如果L1命中则得到结果,如果不命中则向L2寻找,在不命中则向L3,直至主存。在主存中取出所定位的块放入缓存,进行替换。(实际上就是一个逐级寻址,发生不命中则进入下一层)
7.6 hello进程fork时的内存映射
(注:7.6与7.7内容在CSAPP一书中有着明确的描述)
Linux通过将一个虚拟内存区域 与一个磁盘上的对象(object)关联起来,以初始化这个虚拟内存的内容,这个过程被称为虚拟映射(memory mapping)。虚拟内存区域可以映射到良种类型的对象中的一种:Linux文件系统中的普通文件、匿名文件。
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
图7-5 一个共享对象(注意,物理页面不一定是连续的)
7.7 hello进程execve时的内存映射
虚拟内存和内存映射在将程序加载到内存的过程中也扮演者关键的角色。而execve的加载需要以下几个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
4.设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
图7-6 加载器是如何映射用户地址空间的区域的
7.8 缺页故障与缺页中断处理
缺页故障:指当指令引用虚拟地址,当与地址对应的物理页面不被命中(L1直至内存),就成为缺页故障。这个异常导致控制转移到内核的缺页处理程序。
缺页处理程序一共会执行以下步骤:
1.判断虚拟地址A的合法性(假设被引用地址为A)。却也处理程序搜索区域结构的链表,把A和每个区域内的vm_start和vm_end作比较。如果不合法,则程序触发一个段错误,并终止这个进程。
2.判断试图进行的内存访问是否合法。即内存是否有读、写或者执行这个区域内页面的权限、是否由于读写操作导致缺页。如果试图进行的访问不合法,则缺页处理程序会触发一个保护异常,终止这个进程。
3.当已经知道这个缺页是由合法的虚拟地址以及合法的操作造成,此时缺页处理程序选择一个牺牲页面。如果这个牺牲页面被修改过,那么将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送A到MMU。这次MMU就能够正常翻译A,不会再产生缺页中断。
图7-7 Linux缺页处理
7.9动态存储分配管理
动态内存管理的基本方法是通过将放置、分割、合并等基本技术贯穿在许多不同的空闲块组织中,使得动态调用内存的分配更加方便,协调的提高吞吐量及内存利用率。
隐式空闲链表是组成分配器的数据结构,它用来区别快边界、区别已分配快和空闲块。简单模式下,一个块由一个字的头部、有效载荷,以及可能的一些额填充组成。其头部表示了这个快的大小以及这个块的分配与否。而当多个块通过堆组织为一个连续的已分配块和空闲块组成的序列,形成链表形式之后,变成了隐式空闲链表结构。通过分配器遍历堆中的所有块从而间接地遍历整个空闲块的集合。这个链表结构优点在于简单,缺点在于任何操作的开销与堆中已分配快和空闲块的总数呈线性关系。
图7-8-1 一个简单的堆块模式
图7-8-2 隐式空闲链表结构示意图
在放置已分配块时,当一个应用请求k字节的块,分配器搜索空闲链表,找到一个足够大的快来放置所请求的块。分配器执行的搜索方式是由放置策略确定的。常见策略有:首次适配、下一次适配、最佳适配。首次适配就是选取从头搜索的第一个符合要求的块;下一次适配就是从上一次查询结束的地方开始搜索第一个符合要求的块;最佳适配是检查所有快选择最佳者。
在找到合适的空闲块后,将决策分配这个空快中多少空间。如果匹配的不太好就会将空闲块分割为两个部分,然后变成两个新的分配块和空闲块。
图7-8-3 分割一个空闲块,以满足三个字的分配需求
如果分配器找不到一个合适的空闲块,将会有两个选择,一个是通过合并那些在内存中物理上相邻的空闲块来创建一个更大的空闲块。一个是通过调用sork函数,向内核请求额外的内存。
合并空闲块的情况有四种:前空后不空,前不空后空,前后都空,前后都不空。为了提高合并效率,Knuth提出了一种采用边界标记的技术快速完成空闲块的合并。其所用到的结构如下图所示:
图7-8-4 使用边界标记对的堆块格式
使用该格式,分配器可以通过检查脚部,判断前一个快的起始位置和状态,进而将合并分为上述四种情况并进行合并操作。
图7-8-5 四种合并情况
注:情况1为前后都不空,2为前不空后空,3为前空后不空,4为前后都空
还有一种更好的方式,对于通用的分配器,使用显示空闲链表这一结构,其每一个空闲块中都包含一个前驱和后继的指针。这样的话就成为了一个双向链表。使得首次适配的分配时间与空闲块数量线性相关。还可以采用后进先出的顺序或地址增大顺序来维护链表。
图7-8-6 使用双向空闲链表的堆块格式
7.10本章小结
本章讨论与介绍了helo长须运行过程中有关内存管理的过程与知识。讨论了存储管理中,存储器的地址空间、不同的地址翻译过程以得到最终的物理地址、多级页表的虚拟地址到物理地址的转换过程、转换中内存访问涉及到的高速缓存问题、hello进程中fork和execve时内存的映射情况、还讨论了缺页故障和缺页故障管理。最后讨论了动态存储分配管理的过程与相关概念。在这个阶段,为了程序运行的准备时间尽量短、运行性能尽量好,计算机内部各个系统同调配合,hello即将走完他的一生但也收到了许多“朋友的照顾与助力”。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件。一个Linux文件就是一个m字的序列:B0,B1,...,Bk,...Bm-1,所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入输出都被当作对相应文件的读和写来执行。
设备管理:unix io接口。由于将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix IO接口:主要有以下几种来管理Linux的IO设备。
1.打开文件:一个应用要求内核打开文件。宣告它想访问一个I/O设备。内核返回一个小的非负整数,叫描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个文件的所有信息。应用程序只需记住这个描述符。
2.Linux shell创建时每个进程开始时都有三个打开文件:标准输入(描述符0),标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可以用来代替显式的描述符值。
3.改变当前文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行 seek,显式地将改变当前文件位置 k。
4.读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文件,当 k>=m 时,触发EOF(end-of-file)。类似地,一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。
5.关闭文件:当应用完成了对文件的访问后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们。
Unix I/O函数:主要有以下几种函数来对应其接口实现功能。
1.int open(char* filename,int flags,mode_t mode),进程通过调用open函数来打开一个已存在的文件或者创建一个新文件。open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
2.ssize_t read(int fd,void *buf,size_t n)和ssize_t write(int fd,const void *buf,size_t n)分别代表读和写函数。进程调用它们来从文件中执行输入和输出。
3.lseek函数的调用可以满足显示地修改当前文件位置的功能。
4.int close(int fd),进程通过调用close函数来关闭一个打开的文件。fd是需要关闭的文件的描述符。
8.3 printf的实现分析
1.首先对于printf的函数体,给出相关注释分析:
int printf(const char *fmt, ...)//“...”是可变形参的一种写法,表示传递参数个数不确定
{
int i;
char buf[256];
va_list arg = (va_list)((char*)(&fmt) + 4);//va_list是一个字符指针;(char*)(&fmt) + 4代表...中的第一个参数的地址
i = vsprintf(buf, fmt, arg);//vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
write(buf, i);//写操作,并且保存中断前进程的状态
return i;
}
2.vspintf生成显示信息:
int vsprintf(char *buf, const char *fmt, va_list args)
{
char* p;
char tmp[256];
va_list p_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);
}
printf接受一个格式化的命令,并把指定的匹配参数格式化输出,而vsprintf返回的是一个长度即i=vsprintf(buf,fmt,arg),结合后面的write(buf,i)可以看出vsprintf作用就是格式化它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
3.write系统函数:简单来说write函数就是实现写的功能,它告诉编译器要根据各寄存器的值对输出值进行修改。将buf中的i个元素写到终端。
4.syscall的实现比较麻烦,它显示格式化了字符串,并且能够保存中断前进程的状态。
5.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
6.显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
1.对于getchar函数,给出函数主体与注释:
int getchar(void)
{
static char buf[BUFSIZ]; //缓冲区
static char* bb=buf; //指向缓冲区的第一个位置的指针
static int n=0; //静态变量记录个数
if(n==0)
{
n=read(0,buf,BUFSIZ); //读操作
bb=buf; //使得缓冲区第一个位置的指针指向它
}
return(--n>=0)?(unsigned char)*bb++:EOF; //对取操作结束与否进行判断
}
总的来说,大致步骤为:设置缓冲区、定义缓冲区第一个位置的指针、使用读操作读取字符并将指针指向它、对变量个数记录判断并设置终止条件。
2.异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
3.getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了hello的IO管理,介绍与分析了Linux下IO设备管理相关概念、分析了printf、getchar的实现。通过文件的抽象、接口使用、函数的多种设计,程序能够通过简单的操作符进行简单、复杂功能的实现。至此I/O管理给hello先生送了最后一程,hello完成了他的一生!
结论
1.hello在诞生之初,经过程序员从计算机输入编辑得到了程序文件hello.c
2.该文件接下来先要进行预处理环节,经过文件包含、添加行号和文件名标识、宏定义展开与处理、条件编译处理、清理注释内容、特殊控制处理等步骤成为hello.i文件。
3.之后经过编译步骤,经过词法分析、语法分析、语义分析、代码优化生成汇编语言组成的hello.s文件。
4.然后汇编步骤将其转化为hello.o文件,也完全转化为了cpu接受的机械语言。
5.最后经过链接步骤,主要由符号解析、重定位,包含了静态链接和动态链接,最终生成了hello的可执行文件。
6.对于可执行文件,我们进行执行,shell命令行提供了运行可执行程序的指令信息。对于非内置信息,shell就会将其作为程序运行。
7.在程序运行阶段,fork函数被调用,子进程被创建。
8.子进程调用execve将新程序加载到子进程的内部空间,并将子进程的栈、数据等内容更新。根据不同的execve参数,execve调用启动加载器的操作系统代码来对hello(fork出来的子进程)进行执行。随后经历删除子进程区域结构、映射到hello程序的虚拟内存、设置程序计数器PC等步骤。
9.hello的运行需要对地址进行翻译,通过内存管理单元MMU、缓冲后备缓冲器TLB、多级页表、三级Cache等,地址被翻译。
10.在程序的执行阶段,指令引用虚拟地址,当与地址对应的物理页面不被命中(L1直至内存),就成为缺页故障。当出现该故障时,将判断内存和地址合法性,最后使用牺牲页交换缺失页,继续执行。
11.hello程序休眠的实现实际上是内核的上下文切换。
12.在printf执行时,将会调用malloc函数从堆中申请内存。
13.在进程执行期间输入不同字符会触发不同效果。输入Ctrl-C时,shell会向前台作业发送SIGINT信号,该信号会终止前台作业,即hello程序终止执行。当输入Ctrl-Z时,shell会向前台作业发送SIGTSTP信号,该信号会挂起当前进程,即hello程序停止执行,之后再向其发送SIGCONT信号时,hello程序会继续执行。Kill会杀死进程,回车会读入shell命令。
14.程序结束,子进程被回收,内核删除所有相关数据结构
计算机系统是一个通调合作的大集体,每个硬件部件、软件系统在一步步工作中,通过抽象数据类型、具象内存设计来完成一个一个工作。在这次大作业以及这学期的课程中,我第一次深切感受到我面前计算机内部、我的程序内部的运行背后所发生的细节与技术。被被抽象概念与难以理解的技术弄得“焦头烂额”的同时,被计算机系统的精妙设计与高效合作折服。
以我目前肤浅的看法,计算机的设计与实现还是要设计出更加高效率的抽象数据类型以及相适配的处理规则。如I/O设备、Cache等,无不是抽象的具体应用来提高效率。所以我相信这还是一条未来能够发展的路线。
附件
文件名 作用
hello.i .c文件预处理后的文件
hello.s 经编译后含有汇编语言的文本文件
hello.o 经汇编后二进制重定位的目标文件
hello/hello.out 生成的可执行文件
hello.txt hello的反汇编代码存储至txt文件中
参考文献
[1]https://baike.baidu.com/item/%E9%80%BB%E8%BE%91%E5%9C%B0%E5%9D%80/3283849
[2]https://baike.baidu.com/item/%E7%BA%BF%E6%80%A7%E5%9C%B0%E5%9D%80
[3]https://baike.baidu.com/item/%E7%89%A9%E7%90%86%E5%9C%B0%E5%9D%80/2901583
[4]https://baike.baidu.com/item/%E8%99%9A%E6%8B%9F%E5%9C%B0%E5%9D%80
[5]https://baike.baidu.com/item/%E6%AE%B5%E5%BC%8F%E5%AD%98%E5%82%A8%E7%AE%A1%E7%90%86/7291432?fromtitle=%E6%AE%B5%E5%BC%8F%E7%AE%A1%E7%90%86&fromid=15988667&fr=aladdin
[6]https://www.cnblogs.com/Sna1lGo/p/15786602.html
[7]https://www.cnblogs.com/pianist/p/3315801.html
[8]https://blog.csdn.net/albertsh/article/details/89309107
[9]http://lnmp.ailinux.net/readelf
[10]https://wenku.baidu.com/view/1402a37dbcd126fff7050be9.html
[11]https://blog.csdn.net/leikezhu1981/article/details/44831999
[12]《深入理解计算机系统第3版》,Randal E.Bryant,David R.O’Hallaron著,龚奕利,贺莲译,----北京:机械工业出版社,2016.7