摘要: 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下预处理的命令
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 局部变量
本程序中的局部变量为i 和argc 。变量i 没有被赋予初值,在汇编程序中被寄存器ebx代替。变量argc 的内容被保存在寄存器edi中。
3.3.2 对赋值的处理
使用mov 系列指令。本程序中for 循环内的“i=0”即被编译成
3.3.3 对算术操作的处理
本程序中在for 循环内包含有“i++”操作,它被编译成
3.3.4 对关系运算的处理
本程序中的if 条件判断中使用了“argc!=4”,它被编译成
在for 循环中使用了“i<8”,它被编译成
3.3.5 对控制转移的处理
本程序中的if(argc!=4) 条件判断结构通过简单的cmp 系列指令与条件跳转指令完成。它被编译成
其中汇编程序内.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 指令。程序中调用了函数printf ,exit ,sleep ,atoi ,getchar 均使用了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程序中的printf ,exit ,sleep ,atoi ,getchar 函数均被替换,另外还加入了启动、终止相关函数,使程序能够被启动与终止。
根据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利用fork 和execve 执行、被键盘输入的指令中断与挂起或回收的,并且对抢占情况下的上下文切换作出了讨论。
第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 0x80 或syscall 。
之后字符显示驱动子程序通过ASCII码在字模库获取点阵信息,并将其存储至vram中(存储每一个点的RGB颜色信息)。
最后显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户使用键盘输入时,键盘会产生中断请求,传送相应按键的键盘扫描码。中断请求抢占当前进程,内核处理键盘中断处理子程序。系统接收到按键扫描码后将其相应地转成ASCII码,并保存到系统的键盘缓冲区。
getchar 等调用read 系统函数,通过系统调用读取按键ASCII码,直到接受到回车键才返回。
8.5本章小结
本章着重分析hello程序中涉及到的两个与IO有关的函数:printf 和getchar ,并对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