HIT-ICS2020大作业:程序人生-Hello’s P2P

摘要:hello程序是几乎所有人学习计算机编程知识的第一站,然而其生成结果的具体过程并非人尽皆知。本文放眼hello的一生,探究在Linux操作系统下,它从诞生到在内存中的消亡的短暂而又漫长的过程中所经历的种种考验,深入理解它的P2P、O2O历程,以此完成对计算机系统课程的整体复习与深入理解。

关键词:hello程序;计算机系统;预处理;编译;汇编;链接;虚拟内存;异常处理;IO

第1章 概述

1.1 Hello简介

hello的C程序被编写出来之后,经历预处理器、编译器、汇编器、链接器的重重考验,最终生成可执行目标程序。用户在shell中输入相应的指令之后,shell便进行调用fork函数,为其创建子进程。由此,hello经历了从程序(program)到进程(process)的P2P过程。

在执行可执行目标程序hello之前,系统并未为其分配内存。当用户执行它之后,内核为其映射出虚拟内存,并将其载入物理内存。待其运行结束,shell父进程将hello进程回收,内核将相关数据结构删除,hello在内存中所占的空间重新清零。由此,hello经历了从零(0)到零(0)的O2O过程。

1.2 环境与工具

硬件环境:Intel Core i7-8550U x64 CPU;16G RAM;256G SSD;1T HDD
软件环境:Windows 10(64位);VMware Workstation 15;Ubuntu 18.04 LTS(64位)
开发与调试工具:cpp,gcc,ld,记事本,gedit,objdump,edb,terminal,readelf

1.3 中间结果

hello.i                 hello.c经预处理后生成的文件
hello.s                hello.i经编译后生成的文件
hello.o                hello.s经汇编后生成的文件
hello.dis             hello.o经反汇编后重定向到的文件
hello                   hello.o经链接后生成的文件
hello.dis2           hello经反汇编后重定向到的文件

1.4 本章小结

本章对hello一生中完成的P2P过程与O2O过程进行梳理概括,并未接下来对hello一生的探究做了准备。

第2章 预处理

2.1 预处理的概念与作用

概念:预处理器(cpp等)根据以字符#开头的命令,修改原始的C程序。生成另一个以.i为文件扩展名的C程序。

作用:对于原始C程序中的以字符#开头的命令:导入需要使用的库函数;替换宏定义;进行条件编译。

2.2在Ubuntu下预处理的命令

cpp hello.c > hello.i

2.3 Hello的预处理结果解析

预处理结果如图所示:

由图可以看出,在进行预处理之后,原本短短的23行代码被扩充为3113行代码,其中的注释内容不予保留,main函数直到3100行才出现,之前的内容由“#include”开头的内容中指定的库文件内容进行递归替换。

2.4 本章小结

预处理是C语言程序迈向进程的第一步,在这个过程中,预处理器对原始C程序文件进行相关替换与改写,最后生成扩展名为.i的新的C语言程序。

第3章 编译

3.1 编译的概念与作用

概念:编译器(ccl等)将.i文本文件翻译成.s文本文件,它包含一个汇编语言程序。

作用:对替换后的C语言程序进行“翻译”,将其编译成汇编语言,做好后续转换为二进制数的准备。

3.2 在Ubuntu下编译的命令

gcc -m64 -Og -no-pie -fno-PIC hello.i -S -o hello.s

3.3 Hello的编译结果解析

下图是hello.s的内容。

3.3.1 对数据/数据结构的处理

3.3.1.1 字符串

“printf”内的两个字符串“用法: Hello 学号 姓名 秒数!\n”和“Hello %s %s\n”分别存在了.rodata段中的.LC0.LC1中。使用时直接将其作为立即数。

3.3.1.2 数组

在本程序中,使用了系统的数组argv[]argv是指向char型变量指针的指针,且第一个参数是程序名本身。根据指针的长度为8字节和汇编程序可以推测出,argv在寄存器rsi中被取用(之后复制给了寄存器rbp)。使用argv[n]时,对rbp内容进行偏移地址计算后取用(8n(%rbp))。

3.3.1.3 局部变量

本程序中的局部变量为iargc。变量i没有被赋予初值,在汇编程序中被寄存器ebx代替。变量argc的内容被保存在寄存器edi中。

3.3.2 对赋值的处理

使用mov系列指令。本程序中for循环内的“i=0”即被编译成

movl	$0, %ebx

3.3.3 对算术操作的处理

本程序中在for循环内包含有“i++”操作,它被编译成

addl	$1, %ebx

3.3.4 对关系运算的处理

本程序中的if条件判断中使用了“argc!=4”,它被编译成

cmpl	$4, %edi
jne	.L6

for循环中使用了“i<8”,它被编译成

cmpl	$7, %ebx
jle	.L3

3.3.5 对控制转移的处理

本程序中的if(argc!=4)条件判断结构通过简单的cmp系列指令与条件跳转指令完成。它被编译成

cmpl	$4, %edi
jne	.L6

其中汇编程序内.L6标号后的内容对应着C程序中的if成立执行内容。.L6标号内容放在了后面的某个jmp指令之后。

本程序中的for循环结构通过cmp系列指令与条件跳转指令完成。它被编译成

	movl	$0, %ebx
	jmp	.L2
	;...中间代码
.L3:
	;...for循环体汇编代码
	addl	$1, %ebx
.L2:
	cmpl	$7, %ebx
	jle	.L3

可见for循环的在汇编语言中的框架如此,易知循环一共进行8次。

3.3.6 对函数操作的处理

使用call指令和ret指令。程序中调用了函数printfexitsleepatoigetchar均使用了call指令,分别被编译为

call puts
call exit
call printf
call atoi
call sleep
call getchar

调用函数时,均通过寄存器edi传值。

main函数结尾的return 0,被编译成

.cfi_def_cfa_offset 24
popq	%rbx
.cfi_def_cfa_offset 16
popq	%rbp
.cfi_def_cfa_offset 8
ret
.cfi_endproc

3.4 本章小结

本章讲述了扩充后的C程序被编译器编译为汇编语言程序时几种数据与操作编译的主要过程。在编译阶段,编译器根据特定的算法与优化选项,将C程序进行优化合并,对其结构进行调整,“翻译”成汇编语言程序,为下一阶段汇编器的翻译做好准备。

第4章 汇编

4.1 汇编的概念与作用

概念:汇编器(as等)将汇编源程序(.s文本文件)翻译成机器语言指令,把这些指令打包成可重定位目标程序,并将结果保存在.o目标文件中。它是一种二进制文件,无法用文本编辑器正常打开。

作用:通过汇编器(汇编程序)将汇编源程序汇编成二进制可重定位目标程序,为下一步的链接做准备。

4.2 在Ubuntu下汇编的命令

gcc -m64 -Og -no-pie -fno-PIC -c hello.s -o hello.o

4.3 可重定位目标elf格式

分别运行以下指令

readelf -h hello.o
readelf -S hello.o
readelf -S -W hello.o
readelf -r hello.o

可得以下结果

由图可知,hello.o中包含.text节,.rela.text节,.rodata节,.comment节,.eh_frame节,.rela.eh_frame节,.symtab节,.strtab节,.shstrtab节。

ELF头以一个16字节的序列开始(即Magic),这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位,可执行或者共享的),机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。

节头部表描述了其中条目的大小和位置等。

其中典型节的含义:

.text:已编译程序的机器代码;
.rodata:只读数据,如printf语句中的格式串和switch语句的跳转表;
.symtab:存放在程序中定义和引用函数和全局变量信息的符号表;
.rel.text(此处结果为.rela.text):重定位节,保存目标文件与其他文件组合时需要修改位置的.text节中的列表;
.strtab:一个字符串表,内容包括.symtab.debug节中的符号表;
.rela.eh_frame:存放.eh_frame的重定位信息。

4.4 Hello.o的结果解析

使用指令objdump -d -r hello.o > hello.dis到上述反汇编文件hello.dis。将其与hello.s进行对比,可以发现main函数的总体结构没有发生变化,基本可以一一对应。主要区别在于:

1. 分支转移方面,使用偏移地址代替了标号。原因在于标号属于提示性信息,在汇编过程中,不会被保留,执行时根据给出的地址进行相应的跳转。

2. 对.rodata中的字符串取用方面,在编译后的文本文件中,将标号作为立即数直接使用,而在反汇编文件中,使用的是$0x0,并在下方给出重定位信息,对字符串进行访问。

3. 函数调用方面,在反汇编文件中使用了偏移地址代替待调用函数名,并在下方给出了.rela.text中的重定位信息。

4.5 本章小结

本章主要分析了汇编器对汇编源程序的汇编操作结果。汇编器将汇编源程序“翻译”成二进制文件,形成ELF文件,并在头部保留相关信息,便于后续链接操作的进行。对二进制文件进行反汇编,可以发现,程序的大体结构得到保留,但是与之前的汇编源程序之间依然有细微的差别。

第5章 链接

5.1 链接的概念与作用

概念:链接器(ld等)将多个有调用关系的.o程序进行合并,生成一个可执行目标文件。

作用:对.o程序中需要替换的地方进行替换,并重新生成完整的可执行文件。链接使得分离编译成为可能。

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 hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o -o hello

5.3 可执行目标文件hello的格式

运行命令readelf -S -W hello,得图

如上图所示,易得各段名称、类型、起始地址、偏移地址和大小等信息。

5.4 hello的虚拟地址空间

    使用edb加载hello后,发现程序加载地址范围为0x400000~0x4007b3,即从elf开头到.eh_frame段。根据readelf输出的信息,可以在edb的Data Dump中找到对应的数据。下举两例,其他段同理。

由5.3节图,.interp节:0x400200~0x40021c,在edb中的Data Dump见下图;

.text节:0x400550~0x400661,在edb中的Data Dump见下图。

5.5 链接的重定位过程分析

运行指令`objdump -d -r hello > hello.dis2,得到hello的反汇编程序文本hello.dis2,如下图

通过与hello.o的反汇编程序hello.dis比较,可以发现hello的反汇编程序中的地址已经被装配,并且插入了其他库函数内容。

由于在链接时指定了库/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/libc.so,/usr/lib/x86_64-linux-gnu/crtn.o,故hello.o程序中的printfexitsleepatoigetchar函数均被替换,另外还加入了启动、终止相关函数,使程序能够被启动与终止。

根据hello.o中的重定位项目,可以推测出链接器在遇到相应的.plt重定位之前,需要使用的库函数已经被放入了.plt中,故链接器只需要将相应的地址填写到.text节中需要进行替换的位置即可。

.rodata节中的两个字符串的引用过程与之类似,链接器将.rodata中需要调用的内容的地址填入.text中的相应位置即可。

5.6 hello的执行流程

从开始加载到程序终止的整个过程中,调用的所有子程序名与地址如下:

ld-2.27.so!_start                                                        0x7fa3:1e3a5090
ld-2.27.so!_dl_start                                                   0x7f5b:021ecea0
ld-2.27.so!_dl_start_user                                           0x7fa3:1e3a5098
ld-2.27.so!_dl_init                                                     0x7f61:963c9630
libc-2.27.so!_init                                                       0x7fdf:0d1fc920
libc-2.27.so!_dl_vdso_vsym                                     0x7fdf:0d3423b0
ld-2.27.so!_dl_lookup_symbol_x                             0x7fdf:0d5d70b0
ld-2.27.so!do_lookup_x                                            0x7fdf:0d5d6240
ld-2.27.so!strcmp                                                      0x7fdf:0d5e9360
libc-2.27.so!__init_misc                                           0x7fdf:0d2fc6f0
libc-2.27.so!@plt                                                      0x7fdf:0d1fc270
libc-2.27.so@__strrchr_avx2                                    0x7fdf:0d3693c0
libc-2.27.so!__ctype_init                                          0x7f2d:652178f0
libc-2.27.so!init_cacheinfo                                       0x7f2d:65208470
libc-2.27.so!handle_intel.constprop.1                       0x7f2d:652a2e80
libc-2.27.so!intel_check_word.isra.0                        0x7f2d:652a2b80
hello!_start                                                                0x400550
libc-2.27.so!_libc_start_main                                   0x7fb0:ac471ab0
libc-2.27.so!__cxa_atexit                                          0x7fb0:ac493430
libc-2.27.so!__new_exitfn                                        0x7fb0:ac493220
hello!__libc_csu_init                                                       0x4005f0
hello!_init                                                                  0x4004c0
libc-2.27.so!_setjump                                               0x7fb0!ac48ec10
libc-2.27.so!_sigsetjump                                           0x7fb0!ac48eb70
libc-2.27.so!__sigjump_save                                    0x7fb0!ac48ebd0
hello!main                                                                 0x400582
hello!puts@plt                                                           0x4004f0
hello!.plt                                                                    0x4004e0
ld-2.27.so!_dl_runtime_resolve_xsavec                   0x7f2d:655ef750
ld-2.27.so!_dl_fixup                                                  0x7f2d:655e7df0
libc-2.27.so!__GI__IO_file_xsputn                          0x7f2d:65272930
libc-2.27.so!__GI__IO_file_overflow                      0x7f2d:65274330
libc-2.27.so!__GI__IO_doakkocbuf                         0x7f2d:65275300
libc-2.27.so!__GI__IO_doallocate                           0x7f2d:65265100
libc-2.27.so!__GI__IO_file_stat                               0x7f2d:65272180
libc-2.27.so!_fxstat                                                   0x7f2d:652f67b0
libc-2.27.so!.plt+0x2f0                                             0x7f2d:652082c0
libc-2.27.so!malloc                                                   0x7f2d:6527e071
libc-2.27.so!malloc_hook_ini                                   0x7f2d:6527da40
libc-2.27.so!ptmalloc_int.part.0                                0x7f2d:652783e0
libc-2.27.so!_dl_addr                                                0x7f2d:6534cfb0
ld-2.27.so!rtld_lock_default_lock_recursive            0x7f2d:655d90e0
ld-2.27.so!_dl_dind_dso_for_object                         0x7f2d:655ec640
……(此列表不完整)

其中主要函数调用如下:

ld-2.27.so!_start                                                        0x7fa3:1e3a5090
ld-2.27.so!_dl_start                                                   0x7f5b:021ecea0
ld-2.27.so!_dl_start_user                                           0x7fa3:1e3a5098
ld-2.27.so!_dl_init                                                     0x7f61:963c9630
hello!_start                                                                0x400550
libc.2.27.so!__libc_start_main                                  0x7f19:b29f8ab0
libc-2.27.so!__cxa_atexit                                          0x7fb0:ac493430
hello!__libc_csu_init                                                       0x4005f0
hello!_init                                                                  0x4004c0
libc-2.27.so!_setjump                                               0x7fb0!ac48ec10
hello!main                                                                 0x400582
hello!puts@plt                                                           0x4004f0
hello!exit@plt                                                           0x400530
hello!pritf@plt                                                          0x400500
hello!atoi@plt                                                           0x400520
hello!sleep@plt                                                         0x400540
hello!getchar@plt                                                     0x400510
libc-2.27.so!exit                                                        0x7f59:24544120
libc-2.27.so!__run_exit_handlers                             0x7fc5:aa515ed0

5.7 Hello的动态链接分析

调用动态链接共享库函数时,链接器会使用延迟绑定策略,利用GOT和PLT两种数据结构完成对库函数的动态链接。

如图为GOT在dl_init前的内容:

下图为GOT在dl_init之后的内容:

其中GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息,GOT[2]是动态链接器在ld-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,有一个相匹配的PLT条目。

5.8 本章小结

在本章中进行了程序的链接操作,链接包括静态链接和动态链接。并对链接后的程序运行进行了跟踪分析。

第6章 hello进程管理

6.1 进程的概念与作用

概念:一个执行中的程序的实例。

作用:提供给应用程序抽象:一个独立的逻辑控制流、一个私有的地址空间。

6.2 简述壳Shell-bash的作用与处理流程

作用:shell是一个交互型的应用级程序,它代表用户运行其他程序。

处理流程:(读步骤)读取来自用户的一个命令行;(求值步骤)解析命令行;代表用户运行程序;终止。

6.3 Hello的fork进程创建过程

在终端里输入./hello 1180500705 ljh之后,shell接收到用户输入的命令,之后开始进行解析。由于“./hello”不是内置shell命令,shell会转而将其作为路径访问,即运行当前文件夹下的hello程序。当命令的第一个参数解析成功后,shell会创建一个子进程,并在子进程中执行程序。

6.4 Hello的execve过程

execve函数在当前进程的上下文中加载并运行一个新程序。当hello被fork之后,子进程通过execve函数在上下文中加载hello程序,覆盖掉当前进程的地址空间,调用启动代码设置栈,并将控制(命令行参数)传递给该程序的main函数。

6.5 Hello的进程执行

当程序开始执行之后,若当前进程未被抢占,则按照顺序运行,否则进行上下文切换:将当前进程上下文进行保存,恢复将要处理进程的上下文,并将控制传递给要处理的上下文。此时hello进程处于用户态。当执行到sleep函数时,经过atoi函数对数字参数的转换后,进程对sleep函数进行调用,并进入内核模式。内核对当前休眠请求进行处理,并将进程主动释放,进行上下文切换。等到计时结束时,定时器发出中断信号,内核接收之后再进行中断处理,继续运行hello进程,恢复上下文信息。

6.6 hello的异常与信号处理

下图为在运行过程中乱按(包括回车)的效果。在运行过程中进行普通的输入操作,是将输入的内容存入stdin,只有当执行到getchar函数时才会读取一个以“\n”结尾的字符串。之后的内容会被识别为在shell中输入的命令。

下图为输入Ctrl-Z的效果。此时向shell父进程发出了SIGSTP信号,接收到信号后信号处理函数输出提示信息,并将hello进程挂起。

下图为输入Ctrl-Z后运行ps命令的效果,验证了hello进程被挂起而没被回收的结论。

下图为输入Ctrl-Z后运行jobs命令的效果。

下图为输入Ctrl-Z后输入pstree命令的效果,将各个进程关系绘制成了树状图。

下图为输入Ctrl-Z后运行fg命令的效果,将被挂起的hello进程恢复。

下图为输入Ctrl-C的效果。此时向shell父进程发送了SIGINT信号。hello进程被终止并回收。利用ps命令验证了hello进程被终止并回收的结论。

6.7本章小结

本章着眼于生成的hello程序是如何加载到内存之中并被shell利用forkexecve执行、被键盘输入的指令中断与挂起或回收的,并且对抢占情况下的上下文切换作出了讨论。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:hello源程序经编译之后,在汇编程序中的地址;或相对于当前段的偏移地址。
线性地址:hello程序的逻辑地址经段机制转换成的虚拟地址。
虚拟地址:CPU生成的用来访问hello程序的地址,在被送入内存前需要翻译。
物理地址:hello相关程序在物理存储器中具体的单元位置。

7.2 Intel逻辑地址到线性地址的变换-段式管理

段式管理直接将逻辑地址转化为物理地址进行访问。这种地址由段号与偏移地址组成。段选择符由三个字段组成,共16位:请求特权级,表指示标志和索引值。其中表指示标志指出段描述符的位置。段描述符8个字节。段首地址存放在段描述符中,而后者又存放在GDT或LDT中。

7.3 Hello的线性地址到物理地址的变换-页式管理

页式管理的分页机制完成线性地址向物理地址的转换,它对虚拟地址内存空间进行分页。这种转换过程是一个一一映射。

虚拟地址包含虚拟页号(VPN)和虚拟页面偏移(VPO)两部分。CPU上的内存管理单元(MMU)利用VPN选择相应的页表条目,再从中获得物理页号,并将它与VPO结合,就可以求得该虚拟地址对应的物理地址。

7.4 TLB与四级页表支持下的VA到PA的变换

TLB是翻译后备缓冲器,它保存着由单个PTE组成的块。翻译地址时需要现在其中查询是否命中。若命中,则可以直接取得对应的物理地址。

在地址转换时,需要现将VPN分为TLB标记和TLB索引,并检查TLB是否命中。若为命中,则将虚拟地址分为4个VPN和1个VPO。再利用这些索引确定一个物理页号,将之与VPO结合,即可得到物理地址。之后将其存入TLB中。

7.5 三级Cache支持下的物理内存访问

此处以L1 Cache为例,其他Cache同理。

L1 Cache为8路64组相联,块大小64B。易知需要6位CI与6位CO。又VA共52位,故CT为40位。

首先使用CI组索引,每组8路,分别匹配CT。若匹配成功,标志位为1,则命中,根据块偏移CO取得数据并将其返回;否则不命中,向下一级缓存中取出被请求的块,并进行放置。若映射到的组内由空闲块,则直接放置;否则产生冲突,采用最少使用策略对块进行清理替换。

7.6 hello进程fork时的内存映射

当shell进程调用fork函数之后,内核创建新的进程,并为其分配唯一的PID、创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本,并将这两个进程的每个页面都标记为只读,将他们的每个区域结构都标记为私有的写时复制。

7.7 hello进程execve时的内存映射

execve函数被加载时,首先删除已经存在的用户区域,然后为hello程序的代码、数据、栈等创建新的私有的写时复制区域结构,然后再映射共享区域。最后设置程序计数器。

7.8 缺页故障与缺页中断处理

缺页即为DRAM缓存不命中。缺页是会触发一个缺页故障。缺页故障调用内核中的缺页异常处理程序,该程序会选择一个牺牲页。如果它已经被修改了,那么内核会将它复制回磁盘;否则正常写入。之后内核将从磁盘复制,写入内存并更新该页后返回,然后重新启动导致缺页的命令,将导致缺页的虚拟地址重新发送到地址翻译硬件,即可被正常处理。

7.9动态存储分配管理

在运行过程中如果需要额外虚拟内存,则可以使用动态内存分配器。它维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

分配器有两种基本风格:显式分配器、隐式分配器。它们都要求应用显式地分配块。

显式分配器:要求应用显式地释放任何已分配的块;
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。

C标准库提供的显示分配器是malloc程序包。

7.10本章小结

本章讨论了存储在存储器中程序的寻址细节,包括不同地址之间转换的不同方法;还讨论了程序执行时的内存管理细节,包括内存映射和内存分配等。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

文件类型:普通文件——包含任意数据;目录——包含一组链接的文件,其中每个链接都将一个文件名映射到一个文件,这个文件可能是另一个目录;套接字——用来与另一个进程进行跨网络通信的文件;命名通道;符号链接;字符;块设备等。

设备管理:unix io接口

统一方式:打开文件;改变当前的文件位置;读写文件;关闭文件。

8.2 简述Unix IO接口及其函数

open函数将filename转换为一个文件描述符,并且返回描述符数字。用来打开一个已存在的文件或者创建一个新文件。

close函数用来关闭一个打开的文件。

read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。用来执行输入。

write函数从内存buf位置复制至多n个字节到描述符fd的当前文件位置。用来执行输出。

8.3 printf的实现分析

(参考https://www.cnblogs.com/pianist/p/3315801.html

根据printf函数体及其调用函数可知,首先vsprintf生成显示信息,将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。之后利用write系统函数将buf中的i个元素写到终端的函数。然后到陷阱-系统调用int 0x80syscall

之后字符显示驱动子程序通过ASCII码在字模库获取点阵信息,并将其存储至vram中(存储每一个点的RGB颜色信息)。

最后显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

异步异常-键盘中断的处理:当用户使用键盘输入时,键盘会产生中断请求,传送相应按键的键盘扫描码。中断请求抢占当前进程,内核处理键盘中断处理子程序。系统接收到按键扫描码后将其相应地转成ASCII码,并保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ASCII码,直到接受到回车键才返回。

8.5本章小结

本章着重分析hello程序中涉及到的两个与IO有关的函数:printfgetchar,并对Unix的IO系统及其管理进行探究。

结论

在hello短暂的一生中,其实每一个过程都并不简单。

程序员首先将hello的程序编写出来,保存为hello.c文件;

接下来,预处理器将hello.c文件根据其中的宏定义进行扩充和替换,生成hello.i文件,用自动化的方法将小小的程序补充得更加庞大和复杂;

之后,编译器对扩充的hello.i文件进行汇编,并将汇编后的代码保存为hello.s文件;

接着,汇编器将hello.s进行汇编,生成机器语言文件(二进制文件)(可重定位目标程序)hello.o,此时的程序内容,我们的计算机已经能读懂了,只是它依然是残缺的,不能运行;

然后,链接器将hello.o文件与外部现有的共享库文件等链接,将原本残缺的位置填写完整,生成真正的可执行目标文件hello,此时的程序已经是可以被计算机加载并运行的完整文件了。

到了加载与执行的时候,用户需要通过shell(/bash)输入指定格式的命令,由shell调用fork创建子进程、调用execve进行替换,利用虚拟内存管理技术进行寻址和加载,利用异常控制流技术应对用户的“莽撞”。最后,当hello进程运行结束,shell的父进程对其进行回收,将它永远地删除。hello的一生结束了。

这样的一生看似是终结,但只要程序还在,它依然可以在计算机的内存中幻化出一个新的存在,一遍遍地“重生”。这样的终结也不完全是个悲剧。

通过对hello的一生进行的分析,我不由得惊叹计算机和计算机系统发明者的聪明才智,同时也庆幸如今的计算机硬件发展得如此发达,将如此繁多的复杂操作最后呈现得如此简单与迅速。这次探究历程也让我对全书的各项知识进行了复习与思索,让我对计算机系统有了更高、更具体的新认识。

参考文献

[1]  Randel E. Bryant等. 深入理解计算机系统(原书第3版)[M]. 北京:机械工业出版社,2016.

[2]  Acronyms relevant to Executable and Linkable Format (ELF).
https://www.cs.stevens.edu/~jschauma/631/elf.html

[3]  Stackoverflow. Why GCC compiled C program needs .eh_frame section?
https://stackoverflow.com/questions/26300819/why-gcc-compiled-c-program-needs-eh-frame-section

[4]  博客园. [转]printf函数实现的深入剖析.
https://www.cnblogs.com/pianist/p/3315801.html

发表评论