本文主要是介绍HIT 计算机系统大作业——hello的一生,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
摘 要
hello的一生充满了传奇色彩,实现了P2P(从program到progress)的华丽转身,最后实现自己的使命,落叶归根,令人感慨万千。整体来看,在program阶段,他经历了hello.c,hello.i,hello.s,hello.s,hello.o,最终成为可执行文件hello,进入progress阶段。这一阶段中,hello运行在操作系统中,被其抽象为进程。系统利用异常控制流控制进程的运行,通过虚拟内存为hello分配运行空间,提供接口其与OI设备的通信,使得hello能完美地完成自己的使命。
hello完成它的使命后,操作系统利用异常处理子程序则为其“处理后事”,使hello“挥一挥衣袖,不带走一片云彩”
关键词:计算机系统;编译;程序;进程
目 录
第1章 概述... - 4 -
1.1 Hello简介... - 4 -
1.2 环境与工具... - 4 -
1.3 中间结果... - 5 -
1.4 本章小结... - 5 -
第2章 预处理... - 6 -
2.1 预处理的概念与作用... - 6 -
2.2在Ubuntu下预处理的命令... - 6 -
2.3 Hello的预处理结果解析... - 7 -
2.4 本章小结... - 8 -
第3章 编译... - 9 -
3.1 编译的概念与作用... - 9 -
3.2 在Ubuntu下编译的命令... - 9 -
3.3 Hello的编译结果解析... - 9 -
3.3.1数据:... - 11 -
3.3.2非函数操作... - 14 -
3.3.3 函数操作... - 16 -
3.4 本章小结... - 18 -
第4章 汇编... - 19 -
4.1 汇编的概念与作用... - 19 -
4.2 在Ubuntu下汇编的命令... - 19 -
4.3 可重定位目标elf格式... - 19 -
4.4 Hello.o的结果解析... - 21 -
4.5 本章小结... - 24 -
第5章 链接... - 25 -
5.1 链接的概念与作用... - 25 -
5.2 在Ubuntu下链接的命令... - 25 -
5.3 可执行目标文件hello的格式... - 25 -
5.4 hello的虚拟地址空间... - 30 -
5.5 链接的重定位过程分析... - 31 -
5.6 hello的执行流程... - 34 -
5.7 Hello的动态链接分析... - 35 -
5.8 本章小结... - 35 -
第6章 hello进程管理... - 36 -
6.1 进程的概念与作用... - 36 -
6.2 简述壳Shell-bash的作用与处理流程... - 36 -
6.3 Hello的fork进程创建过程... - 36 -
6.4 Hello的execve过程... - 37 -
6.5 Hello的进程执行... - 38 -
6.6 hello的异常与信号处理... - 38 -
6.6.1异常... - 39 -
6.6.2信号处理... - 39 -
6.7本章小结... - 42 -
第7章 hello的存储管理... - 43 -
7.1 hello的存储器地址空间... - 43 -
7.2 Intel逻辑地址到线性地址的变换-段式管理... - 43 -
7.3 Hello的线性地址到物理地址的变换-页式管理... - 44 -
7.4 TLB与四级页表支持下的VA到PA的变换... - 44 -
7.5 三级Cache支持下的物理内存访问... - 47 -
7.6 hello进程fork时的内存映射... - 48 -
7.7 hello进程execve时的内存映射... - 49 -
7.8 缺页故障与缺页中断处理... - 50 -
7.9动态存储分配管理... - 52 -
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 -
结论... - 58 -
附件... - 59 -
参考文献... - 60 -
第1章 概述
1.1 Hello简介
Hello的P2P指hello.c的从代码到进程的过程
hello的P2P意为:从program到progress。
- program:hello在编译系统中经过了五个阶段,它一步步从高级语言变化为可执行程序:从hello.c被预处理到hello.i,从hello.i被编译到hello.s,从hello.s被汇编到hello.o,从hello.o被链接到hello。最终变为一个可执行程序。
图1-1:hello.c编译过程
- progress:可执行文件hello在服务于软硬件交互的操作系统上运行,其被抽象为进程,让hello认为系统上好像只有自己在运行。系统利用异常控制流控制进程的运行,通过虚拟内存为hello分配运行空间,提供接口其与OI设备的通信,使得hello能完美地完成自己的使命。
1.2 环境与工具
硬件环境:
AMD Ryzen 7 4800H;2.90GHz;16G RAM;
软件环境:
Windows10,Ubuntu 20.04
开发与调试工具:
GCC,EDB,GDB,Objdump,readelf,Code:Blocks
1.3 中间结果
hello.c | hello的源文件 |
hello.i | hello.c预处理后的预编译文本文件 |
hello.s | hello.i编译后的汇编文本文件 |
hello.o | hello.s汇编后的可重定位目标文件 |
hello(.out) | hello.o经过链接得到的可执行目标文件 |
hello_elf.txt | hello的ELF格式内容 |
1.4 本章小结
hello的一生可谓P2P,O2O,中间经历了许多步骤进行处理,在接下来的章节中,我们将详细地了解它们。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:
预处理指:在程序源代码被翻译为目标代码的过程中,汇编之前对源程序替换修改的过程。
一般而言,由预处理器对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。
作用:
把以#开头的行解释为预处理指令,读取相应文件代替#开头行插入到程序文本,得到以.i为文件扩展名的预编译文件。
其中ISO C/C++支持的包括:
- #if/#ifdef/#ifndef/#else/#elif/#endif(条件编译);
- #define(宏定义)、#include(源文件包含);
- #line(行控制);
- #error(错误指令);
- #pragma(和实现相关的杂注);
- 单独的#(空指令) 。
预处理使得程序实现:
- 预处理阶段插入头文件;
- 用宏替换相同的变量(宏定义);
- 按不同情况编译不同代码(条件编译);
- 可以阻止编译进行(错误指令)
以便于程序的修改、阅读、调试和移植,也便于实现模块化程序设计
2.2在Ubuntu下预处理的命令
在命令行输入
gcc -E hello.c -o hello.i |
,对hello.c文件进行预处理。
图2-1.hello.c预处理及其生成的hello.i文件
2.3 Hello的预处理结果解析
查看hello.i文件我们可以发现,文件内容非常多,用记事本工具打开后有3000+行,如下:
图2-2.hello.i文件内容(部分)
hello.c只有短短23行,预处理生成的hello.i却有3000+行,这是因为我们在hello.c头部引入了头文件stdio.h、unistd.h和stdlib.h,预处理器将他们替换为系统文件中的内容。
同时,我们也能看到,生成的hello.i文件中,有发现有大量对结构的定义,诸如typedef、struct、enum;对外部变量的引用,诸如extern;对引用目录的标注,诸如”/usr/include/stdio.h”。
图2-3.hello.i中对结构的定义,对外部变量的引用等
2.4 本章小结
本章介绍了预处理阶段的相关概念,作用,并且通过hello实例具体说明预处理操作,并分析了产生hello.i文件。在预处理过程中,进行了头文件引用,删除注释等操作。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:
编译器将hello.i编译成汇编程序文件hello.s。编译的过程实质上是把预处理文件进行词法分析、语法分析、语义分析、优化,从C语言等高级语言转换为汇编语言程序,转换后的文件仍为ASCII文本文件。
作用:
编译后生成的hello.s汇编语言程序文本文件比预处理文件更容易让机器理解,其采用的汇编语言更符合机器的工作逻辑,是生成可执行文件的一步关键过程。
3.2 在Ubuntu下编译的命令
在命令行输入
gcc -S hello.c -o hello.s |
,生成hello.s汇编语言文件
图3-1.编译及其生成的hello.s汇编语言文件
3.3 Hello的编译结果解析
hello.s具体内容如下:
图3-2.hello.s的具体内容
接下来我将逐个解析其中出现的数据与种种操作
3.3.1数据:
3.3.1.1字符常量
字符常量多为字符或字符串
图3-3,字符常量对应汇编语言
图3-3分别为两个字符串提示符,放在静态存储区,不可改变,一直存在。代表“用法: Hello 学号 姓名 秒数!\n”和“Hello %s %s\n”的对应内容
图3-4,字符常量在源程序中的位置
3.3.1.2 变量
(1)局部变量
只在初次使用时赋值,储存在栈中,当所在函数返回,栈会被复原,局部变量在栈中所占的空间也会被释放。
图3-5,局部变量对应的汇编语言
后续程序也多次使用到了这一变量。
图3-6,局部变量后续多次使用
其在源程序的位置如图:
图3-7,局部变量在源程序中的位置
(2)函数参数
即形式参数,其本质为局部变量,只在所调用的函数中起作用。函数调用开始时储存在栈中,当函数返回时,所占空间被释放。
图3-8,函数参数对应汇编语言
图3-9,函数参数在源程序的位置
main将argc和argv作为局部变量存储在栈中,其中:argc为main函数命令行参数的数目,argv为存放参数字符串指针的数组。
3.3.1.3 表达式
C语言的表达式分为很多种,常见的包括:
- 变量常量表达式
- 算数表达式
- 赋值表达式
- 关系判断表达式
- 逻辑表达式
- 复合表达式
其中的变量常量表达式,我们已经在上面的3.3.1.2变量部分讲述完毕;逻辑表达式在程序中没有体现;而复合表达式是其他表达式的复合情况,我们不单独讲述。因此我们讲述余下的三个表达式。
(1)算数表达式
使用运算符(一元或二元)将操作数连接起来的表达式,即类似a+b或i++形式的表达式。
在我们的程序中,i++使i每次循环自增1:
图3-10,自增1算术表达式对应汇编语言
每次循环结束编译器使用ADD指令为储存在栈中的局部变量i增加1。
在源程序中,对应如下:
图3-11,自增1算术表达式在源程序中位置
(2)赋值表达式
使用MOV类指令将一个值赋给另一个地址:
图3-12,赋值表达式对应汇编语言
在源程序中,对应如下:
图3-13,赋值表达式在源程序中位置
(3)关系判断表达式
使用CMP指令对两个值进行对比,然后根据比较结果使用jump指令跳转到相应地址。
图3-14,argc参数个数关系表达式对应汇编语言
图3-15,循环i关系表达式对应汇编语言
在源程序中,对应如下:
图3-16,关系表达式在源程序中的位置
3.3.2非函数操作
3.3.2.1赋值操作
赋值是对数据的一种常见操作,常量、全局变量和静态变量,编译器在程序开始时就已经赋值;对于局部变量,程序在运行过程中对其赋值。而赋值操作和赋值表达式密不可分,因此详见3.3.1.3(2)赋值表达式。
3.3.2.2 算术操作
算数操作和算数表达式密不可分,因此详见3.3.1.3(1)算术表达式。
3.3.2.3关系操作
算数操作和算数表达式密不可分,因此详见3.3.1.3(3)关系表达式
3.3.2.4控制转移操作
使用jump指令进行控制转移,一般出现在判断或循环分支时。不同的条件导致不同的跳转目标,编译器使用cmp指令更新条件码寄存器后,使用相应的jump指令跳转相应位置。
图3-17,hello.s中的控制转移(其1)
其对应C语言中代码
if(argc!=4) |
图3-18,hello.s中的控制转移(其2)
其对应C语言中代码
for(i=0;i<8;i++) |
中
i<8 |
这一部分
图3-19,hello.s中的控制转移(其三)
其对应C语言中代码
for(i=0;i<8;i++) |
中
i=0 |
这一部分,且此为无条件跳转,即将i赋值为0后,无条件进入for开始循环。
3.3.2.5 数组操作
argv为字符串指针数组,其有三个元素,下面展示的hello.s中的代码分别对应argv[2],argv[1],argv[3]。
argv[2] |
argv[1] |
argv[3] |
图3-20,argv数组对应元素
数组元素是储存在堆栈中的,而argv被存储在-32(%rbp),而64位编译下指针大小为8个字节,于是每个参数字符串地址相差8个字节。编译器以数组起始-32(%rbp)为基地址,以偏移量为索引,寻找各个字符串指针的地址。即-32(%rbp)+8,-32(%rbp)+16,-32(%rbp)+24。结果也和调用时的参数寄存器对应的很好,于是能得到相应结论。 |
3.3.3 函数操作
3.3.3.1 参数传递
许多函数都拥有参数,在调用函数之前,编译器会将参数存储在寄存器中,以供调用的函数使用。
函数参数调用遵守以下规则:
①当函数参数个数小于等于6个时,按照以下优先级顺序使用寄存器传递参数:rdi,rsi,rdx,rcx,r8,r9;
②当参数个数大于6个时,前六个参数使用寄存器存放,其他参数压入栈中存储。
我们的程序中多处调用了函数,且涉及到参数传递
(1)当输入参数不为4个时,打印提示信息,将提示字符串的地址赋给rdi寄存器,调用puts函数输出。
图3-21,调用printf传递参数对应汇编语言
这个用例很有意思,在hello.c中明明是使用printf("用法: Hello 学号 姓名 秒数!\n");进行输出的,却在hello.s中调用的是puts函数。
(2)第二次使用printf
即
printf("Hello %s %s\n",argv[1],argv[2]); |
时,对应汇编语言:
第一个参数 |
第二个参数 |
第三个参数 |
图3-22,第二次调用printf传递参数对应汇编语言,及其对应参数
程序用%rdx,%rsi,%rdi分别对应第三个,第二个,第一个参数,这样使得printf得以输出正确的结果。
(3)调用exit函数
将参数1存入%edi中,然后传入exit函数
图3-23,参数传入exit
(4)调用atoi函数
将argv[3]的值从栈中取出到%rdi中,再传入atoi函数中
图3-24,参数传入atoi
(5)调用sleep函数
将atoi函数的返回值从%eax中存到%edi中,作为参数传给sleep
图3-25,参数传入sleep
3.3.3.2 函数调用
程序使用汇编指令call+函数地址调用函数,由于没有重定位而无法得知函数地址,使用函数名作为助记符代替。call将返回地址压入栈中,为局部变量和函数参数建立栈帧,然后转移到调用函数地址。
图3-25,函数调用
3.3.3.3 函数返回
程序使用汇编指令ret从调用的函数中返回,并且还原栈帧。
图3-26,函数返回
说明:此处的函数返回是主函数main的函数返回
3.4 本章小结
本章着重强调了编译这一过程,并对编译生成的汇编程序文件进行了具体而细致的分析。着重解析了hello.s文件中出现的数据与操作,对汇编语言做了深入的研究。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:
汇编器将hello.s文件汇编可重定位目标文件hello.o 。其是一个二进制文件,包含着程序的指令编码
作用:
汇编后生成的hello.o可重定位目标文件离可执行程序更近一步,其为二进制文件,更容易被程序理解,经过链接后可生成可执行文件。
4.2 在Ubuntu下汇编的命令
在命令行输入
gcc -c hello.s -o hello.o |
,生成hello.o可重定位目标文件
图4-1,汇编生成hello.o
4.3 可重定位目标elf格式
在具体介绍ELF之前,我们有必要了解一下ELF文件的整体格式:
图4-2,可重定位目标文件ELF格式
我们利用代码
readelf -a hello.o |
,查看基本信息
(1)ELF头
图4-2,ELF头
ELF头中,列出了Magic数,文件类型,数据组织格式,ELF 文件头版本号,操作系统类型等信息
(2)ELF可重定位目标文件
图4-3,ELF可重定位目标文件
夹在ELF头和节头部表之间的都是节。其中:
.text:已编译程序的机器代码。
.rodata:只读数据,例如printf语句中的格式串和开关语句中的跳转表。
.data:已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中。
.bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。
.symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
.rela.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
为了更深入的了解ELF文件的信息,我们举两个例子具体分析:
- .symtab信息
图4-4,.symtab信息
其中存放:在程序中定义和引用的函数与全局变量的信息。可以看到引用了puts,exit,printf等函数
- .rela.text信息
图4-5,.rela.text信息
其中存放.text节中位置的列表,这其中有许多函数,而且和.symtab中的信息相对应。
4.4 Hello.o的结果解析
输入指令
objdump -d -r hello.o |
,查看hello.o的反汇编
图4-6,hello.o反汇编信息
左侧为16进制机器指令,右侧为对应的汇编指令,与hello.s对比发现两者大体一致,只有以下几点区别
- 伪指令:
hello.s中的开头和结尾有着指导汇编器和连接器的伪指令(以.开头的如.file等)如下图:
图4-7,hello.s开头的伪指令
而hello.o代码只有开头的文件格式说明。
- 分支转移:
只有hello.s代码使用助记符(如.L1)来书写代码,如下图:
图4-8,hello.s使用助记符
而hello.o代码没有使用助记符进行转移,而是使用直接地址跳转。
- 函数调用:
调用函数时,hello.s代码直接使用函数名称作为助记符,如图:
图4-9,hello.s调用函数
而hello.o代码中,由于hello.o文件还未进行重定位,所以其采用符号+数字表示信息,函数的具体地址先使用一串‘0’来代替。
图4-10,hello.o代码调用函数
- 全局变量:
hello.s寻找全局变量时,使用助记符,如.LC0
图4-10,hello.s标记全局变量
而hello.o代码使用0x0(%rip)寻找全局变量的地址,这是因为字符串提示符放在静态存储区,属于全局变量,需要进行重定位,而hello.o还未进行重定位,因此暂时采用偏移0x0代替。
图4-11,hello.o标记全局变量
说明:
通过上述例子我们可以看出,因为hello.o本质上是二进制文件,是机器语言;而hello.s本质上是ASCII文件,是汇编语言,所以二者存在着差异。相较而言,hello.o更贴近机器的运行机制。
4.5 本章小结
本章着重对汇编生成可重定位目标文件hello.o的分析,具体介绍了其ELF文件格式和所包括的内容,对其中具体参数进行了介绍和分析。同时也比较了hello.o和hello.s的异同,更深一步揭开了机器语言和汇编语言的区别。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:
链接程序链接目标程序和用于标准库函数的代码,链接目标程序和由计算机的操作系统提供的资源,把在不同的目标文件中编译的代码收集到一个可直接执行的文件中。
作用:
将模块化编写的程序链接起来,成为一个整体,实现程序功能。这样一来,源程序文件中无需包含共享库的所有代码,可执行文件运行时的内存中只需要包含所调用的函数的代码。极大地方便了程序员,并且增强了程序的空间利用率。
5.2 在Ubuntu下链接的命令
链接的命令为
ld -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/gcc/x86_64-linux-gnu/9/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello.out |
将其输入终端中,得到:
图5-1,hello的链接过程
5.3 可执行目标文件hello的格式
在介绍ELF可执行目标文件之前,我们先了解一下它的典型格式:
图5-2,典型的ELF可执行目标文件
使用
readelf -a hello > hello_elf.txt |
指令将hello的ELF格式输入到文本文件hello_elf.txt中,打开其进行查看。
图5-3,将hello的ELF格式文件输入txt,并查看
5.3.1 ELF头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
hello的ELF头相关信息如下:
图5-4,hello的ELF头信息
hello的ELF以一个16进制序列:
7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
作为ELF头的开头。这个序列描述了生成该文件的系统的字的大小为8字节,并且字节顺序为小端序。
其中的信息表明:ELF头的大小为64字节;目标文件的类型为REL(可重定位文件);机器类型为Advanced Micro Devices X86-64(即AMD X86-64);节头部表的文件偏移为0;以及节头部表中条目的大小,其数量为25。
5.3.2节头部表
图5-5,hello的节头部表(部分)
图5-6,hello的节头部表(部分)
如图5-5,5-6所示,节头部表描述了25个节的相关信息,与hello.o的节头部表相比,多出来的部分:
.interp:动态链接器在操作系统中的位置不是由系统配置决定,也不是由环境参数指定,而是由ELF文件中的 .interp段指定。该段里保存的是一个字符串,这个字符串就是可执行文件所需要的动态链接器的位置,常位于/lib/ld-linux.so.2。
.dynamic:该段中保存了动态链接器所需要的基本信息,是一个结构数组,可以看做动态链接下 ELF文件的“文件头”。存储了动态链接会用到的各个表的位置等信息。
.dynsym:该段与 “.symtab”段类似,但只保存了与动态链接相关的符号,很多时候,ELF文件同时拥有 .symtab 与 .synsym段,其中 .symtab 将包含 .synsym 中的符号。该符号表中记录了动态链接符号在动态符号字符串表中的偏移,与.symtab中记录对应。
.dynstr:该段是 .dynsym 段的辅助段,.dynstr 与 .dynsym 的关系,类比与 .symtab 与 .strtab的关系 hash段:在动态链接下,需要在程序运行时查找符号,为了加快符号查找过程,增加了辅助的符号哈希表,功能与 .dynstr 类似
.rel.dyn:对数据引用的修正,其所修正的位置位于 “.got”以及数据段(类似重定位段 “rel.data”)
.rel.plt:对函数引用的修正,其所修正的位置位于 “.got.plt”。
5.3.3符号表
图5-7,hello的.dynsym(动态符号表)
图5-8,hello的符号表
符号表存放的内容为:在程序中定义和引用的函数和全局变量的信息。存在编号Num、Value、Size、Type、Bind、Vis、Ndx、Name字段。
而且与hello.o文件相比,其中的函数名被替换为更为详细的内容,如puts被替换为puts@@GLIBC_2.2.5,说明其找到了该函数的定义。而且与hello.o相比,多了一个.dynsym节(动态符号表,图5-8)。
5.3.4 重定位信息
图5-8,hello的重定位信息
同样的,与hello.o相比,程序多出来了一个节:.rela.dyn,存放和动态链接相关的信息,是对于数据引用的修正,修正.got及数据段
同时与hello.o相比,程序的许多函数变成了类似 puts@GLIBC_2.2.5+0的形式,说明其在相关的库中找到了这个函数,是对函数引用的修正,用于修正.got.plt
5.4 hello的虚拟地址空间
首先使用edb加载hello,查看虚拟内存的地址,可以发现是从0x400000到0x40500
图5-9,hello虚拟内存地址
(1).interp
对照5.3.2节头部表的相关内容,.interp段地址从0x400318开始,偏移量是0x318,大小为0x1c,对齐要求为1,据此查看0x400318处的内存
图5-10,0x400318地址附近信息
可以看见本处存有动态链接库的位置。
(2).text
对照5.3.2节头部表的相关内容,.text段地址从0x4010f0开始,偏移量是0x10f0,大小为0x1e5,对齐要求为16,据此查看0x4010f0处的内存
图5-11,0x4010f0地址附近信息
与反汇编代码中拥有的机器码比较发现是一致的。
(3).rodata
对照5.3.2节头部表的相关内容,.rodata段地址从0x402000开始,偏移量是0x2000,大小为0x3b,对齐要求为8,据此查看0x402000处的内存
图5-12,0x402000地址附近信息
与反汇编代码中拥有的机器码比较发现是一致的。
5.5 链接的重定位过程分析
使用
objdump -d -r hello |
指令查看反汇编语言,发现hello的反汇编程序与hello.o的反汇编程序相比,多了许多内容,而且也产生了许多不同:
图5-13,采用指令查看反汇编内容
- 代码起始位置
hello的反汇编代码从0x401000开始;而hello.o的反汇编代码从0x0开始
hello.o中,汇编代码从0开始
图5-14,hello.o反汇编代码
hello中,汇编代码从0x401000开始
图5-15,hello反汇编代码
- 引入函数
hello.o的反汇编代码先是.text段,然后就为main函数,且无更多内容;而hello的反汇编代码多了许多的引用,这是由于重定位过程添加进来的,如<puts@plt>,<exit@plt>,<__printf_chk@plt>等等
hello.o中,未引入put函数
图5-16,hello.o反汇编代码
hello中,具体引入了这一函数
图5-17,hello反汇编代码
- 采用虚拟地址
hello.o的反汇编代码在函数跳转时,使用的位置就单单是针对main函数的;而hello的反汇编代码在函数跳转时,采用的是虚拟地址
hello.o中,因为还未重定位,所以先使用0代替
图5-18,hello.o反汇编代码
hello中,已经完成了重定位,采用具体地址代替
图5-19,hello反汇编代码
- 调用.rodata数据
hello程序中的printf静态字符串储存在只读内存段,hello.o中用0x0(%rip)代替,如图。
图5-19,hello.o反汇编代码
而hello中已经完成了重定位,具体标识出了静态字符串的位置
图5-20,hello反汇编代码
链接的过程:
将各个库中用使用到的函数加载到同一文件中,将地址转化为虚拟内存,并重定位到这些函数和静态存储区的数据。
重定位的过程:
hello.o的函数位置,单单是针对main的,且对应位置均为‘0’,重定位后,对应位置指向对应函数;针对静态存储区的数据,会将0x0(%rip)代码段正确替换为正确的位置。
5.6 hello的执行流程
运行hello程序,需要带参数运行,所以要在edb打开后,输入相关参数,如下图所示:
图5-21,edb输入参数
之后正常在每个函数前添加断点,查看运行情况,如下:
1 | 名称:/usr/lib/x86_64-linux-gnu/ld-2.31.so |
2 | 名称:hello!_start |
3 | 名称:lib-2.31.so!__libc_start_main |
4 | 名称:libc-2.31.so!__cxa_atexit |
5 | 名称:/usr/lib/x86_64-linux-gnu/libc-2.31.so |
6 | 名称:hello!__libc_csu_init |
7 | 名称:/home/lyf-120L020901/Desktop/big_project/hello |
8 | 名称:libc-2.31.so!__setjmp |
9 | 名称:libc-2.31.so!__sigsetjmp |
10 | 名称:hello!main |
11 | 名称:hello!__printf_chk@plt |
12 | 名称:hello!__exit @plt |
13 | 名称:hello!__ printf @plt |
14 | 名称:hello!__ sleep @plt |
15 | 名称:hello!__ getchar @plt |
16 | 名称:lib-2.31.so!__exit |
函数调用感觉很乱,但是其内核为:
- 先载入(_dl_start,_dl_init);
- 然后执行初始化过程(_start,_libc_start_main,_init);
- 之后执行主函数(_main,_printf,_exit,_sleep,_getchar);
- 最后退出(_exit)
5.7 Hello的动态链接分析
用edb打开hello,根据hello的ELF格式中.rela.plt的内存位置,找到需要动态链接的函数的地址:
图5-22,.rela.plt内容
因此去edb对应区域查看,在do_init调用前,发现对应区域为空:
图5-23,do_init调用前
在do_init调用后,发现对应区域成功链接,产生了相关内容:
图5-24,do_init调用后
由此可以看到,在do_init执行后,对应地址被赋予了相应偏移量的值,说明完成了动态链接
5.8 本章小结
本章主要讲解了链接的过程,着重分析了hello的执行过程,同时也分析了hello的ELF格式及其中的内容,同时分析了虚拟空间,重定位,动态链接等问题。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程的经典定义是一个执行中程序的实例,系统的每个程序都运行在某个进程的上下文。
而上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存里的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。
作用:
通过进程,我们会得到一种假象,好像我们的程序是当前唯一运行的程序,我们的程序独占处理器和内存,我们程序的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash的作用:
Shell是一个命令解释器,是用户使用Linux的桥梁,它解释由用户输入的命令并且把它们送到内核,提供用户与内核进行交互操作的一种接口,并且允许用户编写由shell命令组成的程序。
处理流程:
用户输入键盘信号,shell应该接受这些键盘输入信号,并对这些信号进行相应处理:
- 从终端读入输入的命令,将输入字符串切分获得所有的参数;
- shell对用户输入命令进行解析,判断是否为内置命令,如果是则立即执行;
- 若不是内置命令,则会检查是否为一个可执行文件,如果是,则会fork子进程,启动加载器在当前进程中加载并运行程序;
- 如果不是内置命令且无法找到这个可执行文件,则会显示一条错误信息;
- 如果程序是前台运行程序,则调用等待函数等待前台作业结束;否则将程序转入后台,直接开始下一次用户输入命令;
6.3 Hello的fork进程创建过程
我们在shell上输入./hello,这不是一个内置的shell命令,所以shell会认为hello是一个可执行目标文件,通过调用加载器的操作系统代码来运行它。
图6-1,输入./hello
当shell运行一个程序时,父进程通过fork函数生成这个程序的进程。新创建的子进程几乎但不完全与父进程相同,包括代码、数据段、堆、共享库以及用户栈。父进程和新创建的子进程之间最大的区别在于他们有不同的PID。
图6-2,fork的定义
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序
图6-3,execve的定义
execve函数加载并运行可执行文件filename(hello),且带参数列表argv和环境变量envp.当加载器运行时,它创建一个与下图(图6-4)类似的的内存映像。
图6-4,内存映像
在程序头部表的引导下,加载器将可执行文件的片复制到代码段和数据段,之后,加载器跳转到程序的入口:_start函数的地址。_start函数调用系统启动函数,_libc_start_main(该函数定义在libc.so里),之后初始化环境,调用用户层的main函数,处理main函数返回值,并且在需要的时候返回给内核。
6.5 Hello的进程执行
想了解进程执行,先了解一些基本概念:
- 多个流并发地执行的一般现象被称为并发。
- 一个进程和其他进轮流运行的概念称为多任务。
- 一个进程执行它的控制流的一部分的每一时间段叫做时间片。
- 程序在进行一些操作时会发生内核与用户状态的不断转换。这是为了保持在适当的时候有足够的权限和不容易出现安全问题。
操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。上下文就是内核重启一个被抢占的进程所需要的状态。
在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个决策称为调度。调度的过程是由调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,并进行:
1)保存以前进程的上下文
2)恢复新恢复进程被保存的上下文
3)将控制传递给这个新恢复的进程,来完成上下文切换。
这样,就完成了一次调度,下图(图6-5)为一个例子:
图6-5,上下文切换
hello程序与操作系统其他进程通过操作系统的调度,切换上下文,拥有各自的时间片从而实现并发运行。针对我们的hello程序而言,sleep函数的调度就是这样一个过程:
1)hello初始运行在用户模式,在hello进程调用sleep之后陷入内核模式;
2)内核处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时;
3)内核进行上下文切换将当前进程的控制权交给其他进程;
4)当计时完成时,发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态;
5)至此,hello进程就可以继续进行自己的控制逻辑流了。
6.6 hello的异常与信号处理
6.6.1异常
hello执行过程中可能出现四类异常:中断、陷阱、故障和终止。
(1)中断是异步发生的,是来自外部I/O设备的信号的结果,中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码,就像没有发生过中断一样。
(2)陷阱是同步发生的,是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。
(3)故障是同步发生的,是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。
(4)终止是同步发生的,是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。
6.6.2信号处理
6.6.2.1不停乱按,包括回车
如果乱按过程中没有回车,这个时候只是把输入屏幕的字符串缓存起来,如果输入最后是回车,getchar读回车,并把回车前的字符串当作shell输入的命令,如下图所示:
图6-6,不停乱按,包括回车
6.6.2.2 Ctrl+Z
输入Ctrl+Z会发送一个SIGTSTP信号给前台进程组的每个进程,结果是停止前台进程——即hello程序 ,如下图:
图6-7,Ctrl+Z
6.6.2.3 Ctrl+C
输入Ctrl+C会让内核发送一个SIGINT信号给到前台进程组中的每个进程,结果是终止前台进程——即hello程序,通过ps命令发现这时hello进程已经被回收,如下图:
图6-8,Ctrl+C
6.6.2.4 Ctrl+Z后可以运行ps jobs pstree fg kill 等命令
Ctrl+Z使前台进程暂停,我们将逐个尝试ps jobs pstree fg kill 等命令:
(1)Ctrl+Z后运行ps
如图,ps会列出当前正在执行的进程(包括暂停的进程),此时可以看到进程的名称和PID号
图6-9,Ctrl+Z后运行ps
(2)Ctrl+Z后运行jobs
如图,我们暂停的程序被正确的显示,而且后面列出了相应的参数,且标记为“Stopped”
图6-10,Ctrl+Z后运行jobs
(3)Ctrl+Z后运行pstree
如图,可以清晰地看到进程树之间的关系,可以清楚的看出来是谁创建了谁,即哪个进程是父进程,哪个是子进程
图6-11,Ctrl+Z后运行pstree
(4)Ctrl+Z后运行fg
如图,fg的功能是使第一个后台作业变为前台,而第一个后台作业是hello,所以输入fg 后hello程序又在前台开始运行,并且是继续刚才的进程,输出剩下的7个字符串。
图6-12,Ctrl+Z后运行fg
(4)Ctrl+Z后运行kill
如图,先使用ps查看hello进程的PID然后使用
kill -9 11516 |
强制杀死hello进程,再次用ps查看,发现hello程序已经被终止。
图6-13,Ctrl+Z后运行kill
6.7本章小结
本章介绍了进程的概念和作用,讲述了shell的基本操作,还总结了shell是如何利用fork新建子进程,如何利用execve加载进程,如何进行上下文切换。重点分析了hello的异常种类,并且利用具体事例分析了其信号处理。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:
源代码经过预处理、编译、汇编后出现在汇编程序中地址,包含在机器语言中用来指定一个操作数或一条指令的地址。
每一个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。
线性地址:
地址空间是一个非负整数的有序集合,如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间。
线性地址就是线性地址空间中的地址。是程序中的虚拟内存地址。
虚拟地址:
在采用虚拟内存的系统中,CPU从一个有N = 2n个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间,里面的地址就是虚拟地址。
物理地址:
在实际的物理空间中的地址,叫物理地址。CPU通过地址总线的寻址,找到真实的物理内存对应地址。在前端总线上传输的内存地址都是物理内存地址。地址翻译会将hello的一个虚拟地址转化为物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在 Intel 平台下,以Core i7为例,其内存系统如下图所示:
图7-1,Core i7内存地址
以下为三个基本概念,能帮助我们理解线性地址:
- 一个逻辑地址由段标识符和段内偏移量组成;
- 段标识符是一个16位长的字段。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
- 全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。
对于给定一个完整的逻辑地址段选择符+段内偏移地址,确定线性地址的顺序如下:
- 看段选择符的T1= = 0还是= = 1,知道当前要转换是GDT中的段,还是LDT中的段;
- 再根据相应寄存器,得到其地址和大小;
- 拿出段选择符中前13位,在这个数组中,查找到对应的段描述符,由此得到了其基地址;
- Base + offset = 线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
对于页式管理,我们首先要了解:线性地址(虚拟地址)由虚拟页号VPN和虚拟页偏移VPO组成。
而从线性地址到物理地址的变换,过程如下:
- 首先,MMU从线性地址中抽取出VPN,并且检查TLB,看他是否因为前面某个内存引用缓存了PTE的一个副本。
- TLB从VPN中抽取出TLB索引和TLB标记,查找对应组中是否有匹配的条目。
- 若命中,将缓存的PPN返回给MMU;若不命中,MMU需从页表中的PTE中取出PPN。
- 若得到的PTE无效或标记不匹配,就产生缺页,内核需调入所需页面,重新运行加载指令;若有效,则取出PPN。
- 最后将线性地址中的VPO与PPN连接起来就得到了对应的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
先介绍一下TLB:
每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。
TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单一PTE组成的块。TLB通常有高的相联度,从虚拟地址中的页号提取出组选择和行匹配的索引和标记字段。
当TLB命中时,包含的步骤有如下四步:
- CPU产生一个虚拟地址;
- MMU从TLB中取出相应的PTE;
- MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存;
- 高速缓存/主存将所请求的数据字返回给CPU
图7-2,TLB命中
当LTB不命中时,MMU必须从L1缓存中取出相应的PTE,新取出的PTE存放在TLB中,可能会覆盖一个已经存在的条目。
图7-3,TLB不命中
然后再介绍一下多级页表:
多级页表第一、二、三级条目格式如下:
图7-4,多级页表第一、二、三级条目格式
第四级条目格式如下:
图7-5,多级页表第四级条目格式
将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。
如下图(图7-6),VPN被分为4部分,第一级VPN结合基址寄存器得到一个页表条目,其中存放下一级页表的基址,再结合VPN2,得到第三级页表基址,继续寻找,以此类推,直到最后确定对应的物理页号,再与VPO结合,得到由PPN与PPO结合成的物理地址,用于物理地址寻址。
图7-6,四级页表支持下的虚拟地址到物理地址的转换
7.5 三级Cache支持下的物理内存访问
如图,下图是一个典型的Cashe结构
图7-7,典型Cashe结构
对于一个虚拟地址请求,在三级Cashe下,变为物理地址要经历如下步骤:
-
- 先去TLB寻找,如果命中的话就直接去MMU获取;
- 如果未命中,就会结合多级页表得到物理地址,去cache中寻找;
- 先到L1,检测是否命中,不命中则紧接着寻找下一级cache L2,接着L3;相当于一级一级往下找,直到找到对应的内容,
在三级Cashe支持下,这样操作可以加快程序运行的速度。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的:
- mm_ struct(内存描述符):描述了一个进程的整个虚拟内存空间;
- 区域结构;
- 页表的原样副本。
它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
而写时复制是一个巧妙的技术, 他将私有对象巧妙的映射到虚拟内存中。在物理内存中,只保存有私有对象的一份副本。比如,下图:
图7-8,物理内存保存有私有对象的一份副本
在此种情况下,两个进程将一个私有对象映射到它们虚拟内存的不同区域,但是共享这个对象使用同一个物理副本。对于每个进程,相应私有区域的页表条目都被标记为只读,并且区域结构被标记为私有的写时复制。只要有一个进程试图写私有区域内的某个页面,那么这个写操作就会触发个保护故障。如下图:
图7-9,尝试进行写入
当进程尝试写私有的写时复制区域中的内容,故障处理程序就会开始工作,它会在物理内存中创建这个页面的一个新副本,并更新页表条目指向这个新的副本,然后恢复这个页面的可写权限。当故障处理程序返回时,CPU重新执行这个写操作,即可正常执行。
因为存在写时复制的机制,当fork在新进程中返回时,新进程现有的虚拟内存和调用fork时存在的虚拟内存相同。当这两个进程中的任意一个进行写操作时,写时复制机制就会创建新页面。这样一来,便实现了进程的“私有地址空间”。
7.7 hello进程execve时的内存映射
execve 函数的函数声明为int execve(char *filename ,char *argv[], char *envp[]);
execve 函数在当前进程中加载并运行filename程序,用其有效地替代了当前程序。而这需要以下几个步骤:
- 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
- 映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。具体区域和内容如下图所示:
图7-10,新区域内容
- 映射共享区域。如果filename程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
当再次调度这个进程时,它将从入口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
在虚拟内存常见说法中,DRAM缓存不命中称为缺页。在缺页之前,页表状态如下:
图7-11,缺页之前页表状态
CPU想要引用VP3中的一个字,但是VP3并未缓存在DRAM中,通过读取PTE3,发现有效位为0,说明不在内存里,这时就发生了缺页异常。这时会调用缺页异常处理程序,该程序会选择一个牺牲页,本例中为VP4,而且如果VP4已经被修改,内核会将它复制回磁盘。
此时VP4已经不再缓存在主存中,之后操作如图:
图7-12,缺页之前页表状态
接下来,内核从磁盘复制VP3到内存中的PP3,更新PTE3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,此时可以正常执行。
值得注意的是:缺页处理程序不是直接就替换,它会经过一系列的步骤,如图:
图7-13,缺页处理程序
- 段错误:虚拟地址是合法的吗?如果不合法,它就会触发一个段错误
- 保护异常:试图进行的内存访问是否合法?意思就是进程是否有读,写或者执行这个区域的权限
经过上述判断,这时才能确定这是个合法的虚拟地址,然后才会执行正常缺页替换。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap),见下图:
图7-14,堆
堆是一个二进制零的区域,它紧接在未初始化的数据区域后开始,并向更高的地址生长。对于每个进程,内核维护着一个变量brk(读做“break"), 它指向堆的顶部。
分配器将堆视为一组不同大小的块(block)的集合来维护,有如下特点:
- 每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。
- 已分配的块显式地保留为供应用程序使用。
- 空闲块可用来分配。直到它显式地被应用所分配。
- 一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
①显式分配器。它要求应用显式地释放任何已分配的块。例如: C程序通过调用 malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。
图7-15,C语言中malloc函数的定义
②隐式分配器。 此种分配器能自主检测一个已分配块,如果其不再被程序所使用,那么就释放这个块。例如,诸如Lisp,ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
7.10本章小结
本章着重强调了hello的存储管理,介绍了逻辑地址,线性地址,虚拟地址,物理地址。同时也介绍了几种常用的地址变换的模式。也说明了fork和execve的内存映射,介绍了缺页故障和缺页中断处理的方法,最后介绍了动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。
设备的模型化:文件
设备管理:Unix IO接口
8.2 简述Unix IO接口及其函数
接口的定义:
接口就是连接CPU与外设之间的部件,它完成CPU与外界的信息传送。
接口的十大功能:
- 输入输出功能
- 数据缓冲功能
- 联络功能
- 数据转换功能
- 中断管理功能
- 提供时序控制功能
- 寻址功能
- 可编程功能
- 电器特征的匹配功能
- 错误监测功能
Unix I/O接口使得所有的输入输出都能以一种统一且一致的方式来执行:
- 打开文件:应用程序如果想要访问一个I/O设备,则会要求内核打开相应的文件。内核返回一个小的非负整数——描述符,它在后续对此文件的所有操作中标识这个文件,并且内核记录有关此打开文件的所有信息。
- shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。
- 改变当前的文件位置:对于每个打开的文件,内核保存着一个初始为0的文件位置k,它是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地将改变当前文件位置k。
- 读写文件:
1)读操作就是从当前文件位置k开始,从文件中复制n(n>0)个字节到内存,然后将k增加到k+n。若文件大小为m字节,当k>=m时,将会触发EOF。
2)写操作就是从内存中复制n(n>0)个字节到一个文件,这将会从当前文件位置k开始,然后更新k。
- 关闭文件:内核会释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix I/O接口函数:
- open函数:
图8-1,open函数
open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。
flags参数指明了进程打算如何访问这个文件。
mode参数指定了新文件的访问权限位。作为上下文的一部分,每个进程都有一个umask,它是通过调用umask函数来设置的。当进程通过带某个mode参数的open函数调用来创建一个新文件时,文件的访问权限位被设置成mode&~umask。
- close函数:
图8-2,close函数
成功将返回0;错误将返回EOF
- read函数:
图8-3,read函数
read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。
返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
- write函数:
图8-4,write函数
write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
printf代码段如下:
图8-5,printf函数
printf需要做的事情是:接受一个fmt的格式,然后将匹配到的参数按照fmt格式输出。printf用了两个外部函数,一个是vsprintf,还有一个是write。这两个函数的作用分别如下:
- vsprintf函数:接受确定输出格式的格式字符串fmt(输入),用格式字符串对个数变化的参数进行格式化,产生格式化输出。
- write函数:将buf中的i个元素写到终端。
在write函数中,将参数传入后,函数会保存中断前进程状态,然后将输出信息压入栈中,之后调用sys_call,把待输出内容输出到屏幕上,这个过程大致包括:
- 调用字符显示驱动子程序,从ASCII转换到字模库,再转换到vram(存储每一个点的RGB颜色信息)。
- 显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
由此便实现了printf的功能。
8.4 getchar的实现分析
getchar代码如下:
图8-6,getchar函数
当程序调用getchar函数时,会等待用户的输入,用户的输入会被存放在键盘缓冲区直至按下回车。按下回车时,getchar以字符为单位读取字符缓冲区,但不会读取回车键和文件结束符,假如在键入回车前输入了不止一个字符,其他字符会保留在键盘缓冲区。
异步异常-键盘中断的处理:
- 我们进行键盘输入时,会从当前进程跳转到键盘中断处理子程序,接受按键扫描码。
- 当键盘上的一个按键按下时,键盘会发送一个中断信号给CPU,与此同时,键盘会在某一指定端口输出一个数值,这个数值对应按键的扫描码叫通码;
- 当按键弹起时,键盘又给端口输出一个数值,这个数值叫断码;
- 这样计算机知道我们何时按按下、松开按键,以及是否一直按着按键。
- 中断处理子程序把键盘的通码和断码数值转换为按键编码(对于字母键、数字键为ASCII码),缓存到键盘缓冲区;
- 然后把控制器交换给原来任务,若没有遇到回车键,继续等待用户输入,重复上述过程;
- 遇到回车键后,getchar函数按字节读取键盘缓冲区内的内容,处理完毕后getchar返回,getchar进程结束。
8.5本章小结
本章注重分析了IO管理,介绍了Unix IO接口及其函数,同时对printf,getchar的实现进行了具体分析,具体解释。
(第8章1分)
结论
hello所经历的过程:
hello的一生充满了传奇色彩,实现了P2P(从program到progress)的华丽转身,最后实现自己的使命,落叶归根,令人感慨万千。
- program:hello在编译系统中经过了五个阶段,它一步步从高级语言变化为可执行程序:从hello.c被预处理到hello.i,从hello.i被编译到hello.s,从hello.s被汇编到hello.o,从hello.o被链接到hello。最终变为一个可执行程序。
- progress:可执行文件hello在服务于软硬件交互的操作系统上运行,其被抽象为进程,让hello认为系统上好像只有自己在运行。系统利用异常控制流控制进程的运行,通过虚拟内存为hello分配运行空间,提供接口其与OI设备的通信,使得hello能完美地完成自己的使命。
我的创新理念:
追踪函数运行时,不仅使用edb,还使用gdb,避免了edb部分函数编码不清的问题。
实际进行操作,查阅多方资料进行分析。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c | hello的源文件 |
hello.i | hello.c预处理后的预编译文本文件 |
hello.s | hello.i编译后的汇编文本文件 |
hello.o | hello.s汇编后的可重定位目标文件 |
hello(.out) | hello.o经过链接得到的可执行目标文件 |
hello_elf.txt | hello的ELF格式内容 |
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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] 劳复思. 计算机是如何将一个字符显示到显示器上的[EB/OL]. http://blog.sina.com.cn/s/blog_576245b00102xxv9.html,2018-03-30/2021-06-23.
[8] ldjsld. 字符显示器的显示控制过程[EB/OL]. http://www.360doc.com/content/16/0730/16/152409_579583865.shtml,2016-07-30/2021-06-23.
[9] 快乐之家. 输入输出设备与输入输出系统[DB/OL]. https://www.docin.com/p-7202672.html,2009-01-29/2021-06-23.
[10] xumingjie 1658. 键盘中断的处理过程[EB/OL]. https://blog.csdn.net/xumingjie1658/article/details/6965176,2011-11-13/2021-06-23.
(参考文献0分,缺失 -1分)
这篇关于HIT 计算机系统大作业——hello的一生的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!