计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 数据科学与大数据技术
学 号 2022111436
班 级 2203501
学 生 zoCbo
指 导 教 师 吴锐
计算机科学与技术学院
2024年5月
本文深入分析了Hello程序,带领读者了解了hello程序的生命周期——从源代码到可执行文件,再到实际运行,从而为读者揭示计算机系统的奥妙。
Hello程序虽然只是无数程序员入门的第一段程序,计算机系统想要实现它却要付出不少的努力。首先是预处理阶段,初步转换和优化了代码中的指令,接下来通过编译将C语言代码转换为了更贴近机器的汇编语言程序。接下来又进一步将汇编语言转换为机器语言,成为了可重定位的目标文件。而在链接阶段中,我们将系统中原有的静态库,头文件等与目标文件链接成一个整体,从而得到了可执行的文件。接下来是运行阶段,本文介绍了hello程序在linux系统下运行的整个生命周期,介绍了P2P以及020的过程,展现了进程管理、I/O管理等运行方式。
本文研究hello程序,旨在窥一斑而见全豹,从一个小程序了解到整个计算机系统中程序的生命历程,进而全面的了解计算机系统的工作原理,将理论知识在脑海中具象化。
关键词:计算机系统;linux;预处理;链接;汇编;编译;P2P;020;
目 录
第1章 概述................................................................................... - 4 -
1.1 Hello简介............................................................................ - 4 -
1.2 环境与工具........................................................................... - 4 -
1.3 中间结果............................................................................... - 4 -
1.4 本章小结............................................................................... - 4 -
第2章 预处理............................................................................... - 5 -
2.1 预处理的概念与作用........................................................... - 5 -
2.2在Ubuntu下预处理的命令................................................ - 5 -
2.3 Hello的预处理结果解析.................................................... - 5 -
2.4 本章小结............................................................................... - 5 -
第3章 编译................................................................................... - 6 -
3.1 编译的概念与作用............................................................... - 6 -
3.2 在Ubuntu下编译的命令.................................................... - 6 -
3.3 Hello的编译结果解析........................................................ - 6 -
3.4 本章小结............................................................................... - 6 -
第4章 汇编................................................................................... - 7 -
4.1 汇编的概念与作用............................................................... - 7 -
4.2 在Ubuntu下汇编的命令.................................................... - 7 -
4.3 可重定位目标elf格式........................................................ - 7 -
4.4 Hello.o的结果解析............................................................. - 7 -
4.5 本章小结............................................................................... - 7 -
第5章 链接................................................................................... - 8 -
5.1 链接的概念与作用............................................................... - 8 -
5.2 在Ubuntu下链接的命令.................................................... - 8 -
5.3 可执行目标文件hello的格式........................................... - 8 -
5.4 hello的虚拟地址空间......................................................... - 8 -
5.5 链接的重定位过程分析....................................................... - 8 -
5.6 hello的执行流程................................................................. - 8 -
5.7 Hello的动态链接分析........................................................ - 8 -
5.8 本章小结............................................................................... - 9 -
第6章 hello进程管理.......................................................... - 10 -
6.1 进程的概念与作用............................................................. - 10 -
6.2 简述壳Shell-bash的作用与处理流程........................... - 10 -
6.3 Hello的fork进程创建过程............................................ - 10 -
6.4 Hello的execve过程........................................................ - 10 -
6.5 Hello的进程执行.............................................................. - 10 -
6.6 hello的异常与信号处理................................................... - 10 -
6.7本章小结.............................................................................. - 10 -
第7章 hello的存储管理...................................................... - 11 -
7.1 hello的存储器地址空间................................................... - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理................... - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理............. - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换.................... - 11 -
7.5 三级Cache支持下的物理内存访问................................ - 11 -
7.6 hello进程fork时的内存映射......................................... - 11 -
7.7 hello进程execve时的内存映射..................................... - 11 -
7.8 缺页故障与缺页中断处理................................................. - 11 -
7.9动态存储分配管理.............................................................. - 11 -
7.10本章小结............................................................................ - 12 -
第8章 hello的IO管理....................................................... - 13 -
8.1 Linux的IO设备管理方法................................................. - 13 -
8.2 简述Unix IO接口及其函数.............................................. - 13 -
8.3 printf的实现分析.............................................................. - 13 -
8.4 getchar的实现分析.......................................................... - 13 -
8.5本章小结.............................................................................. - 13 -
参考文献....................................................................................... - 16 -
第1章 概述
1.1 Hello简介
P2P:
Hello程序的生命是从C语言代码开始的。Hello.c程序首先通过预处理器cpp得到hello.i。然后通过编译器cll将hello.i变成汇编程序hello.s。再通过汇编器as将hello.s变成可重定位目标程序hello.o。最后通过链接器将标准库中的函数合并到hello.o文件中,就得到了可执行目标文件hello.o。运行时,在shell中通过fork函数创建子进程,通过execve加载此程序,此时hello成为了进程,从program到process。
O2O:
系统通过调用excave函数加载hello程序,为其创建虚拟内存,并将虚拟内存映射载入物理内存,进入CPU处理。CPU为其分配时间片,进行流水线操作。在程序结束运行后,父进程回收子进程,删除创建的虚拟内存及相关数据,将其从系统中清除,此时完成从zero到zero。
1.2 环境与工具
1.2.1 硬件环境
处理器:Inel Core i7 12700H
RAM:16GB
1.2.2 软件环境
Windows11 64位
1.2.3 开发工具
CodeBlocks,Vscode,Visual Studio
Gcc、g++;
1.3 中间结果
hello.c C语言源文件
hello.i 预处理产生的文件
hello.s 编译产生的汇编代码文件
hello.o 汇编产生的可重定位目标文件
hello.o.asm hello.o反汇编生成的文本文件
hello.asm hello反汇编生成的文本文件
hello 链接产生的可执行目标文件
hello.elf hello文件的ELF格式
hello.o.elf hello.o文件的ELF格式
1.4 本章小结
介绍了hello程序from program to process(P2P)和from zero to zero(020)的过程。同时说明了此论文用到的环境与工具,还有生成的文件信息。
。
第2章 预处理
2.1 预处理的概念与作用
概念:程序预处理是在编译或解释之前对源代码进行的一系列操作,旨在简化、优化和准备源代码以便于编译器更好地理解和处理源代码。预处理器对#开头的指令做解释。
C语言的预处理器是编译器的一部分,它负责在实际的编译之前对源代码进行处理。以下是C语言中预处理器的主要内容:
1.宏替换:预处理器允许使用#define指令定义宏,然后在源代码中使用这些宏进行替换。例如:
图2.1.1
在这个例子中,PI和SQUARE()都会在编译前被替换为相应的值。
2.条件编译:通过条件编译指令,可以根据条件选择性地包含或排除特定的代码块。常用的条件编译指令有#ifdef、#ifndef、#if、#else、#elif和#endif。例如:
图2.1.2
3.头文件包含:通过#include指令可以包含其他文件的内容。通常用于包含函数声明、宏定义和结构声明等。例如:
图2.1.3
4.文件包含保护:预处理器提供了文件包含保护机制,以防止同一文件被包含多次。这通常通过预定义宏和条件编译指令实现。例如:
图2.1.4
5.行连接和多行宏:预处理器允许使用行连接符\将一行代码拆分成多行。此外,可以使用反斜杠\将多行组成一个宏。例如:
图2.1.5
6.预定义宏:预处理器提供了一些预定义的宏,如__FILE__、__LINE__和__DATE__等,用于获取文件名、行号和编译日期等信息。
这些是C语言中预处理器的主要内容,它们使得在编译前能够对源代码进行一些必要的处理,以便于后续的编译、调试和执行。
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
图2.2.1
2.3 Hello的预处理结果解析
图2.3.1 hello.i程序开头和结尾的截图
可知经过预处理后,hello.c中预处理指令消失了,而多出了3000多行代码,说明程序的预处理事实上是把头文件代码与hello.c文件拼接在一起,从而形成了hello.i文件。
hello.i还引入了类型定义以及结构体:
图2.3.2 类型定义
结构体:
2.3.3 结构体
2.4 本章小结
本章介绍了预处理的概念,并分析了预处理所产生的hello.i文件,发现了hello.i文件与hello.c的不同之处,明白了预处理的原理,知道了预处理主要是处理以#开头的预处理指令。
第3章 编译
3.1 编译的概念与作用
概念:
编译是将高级语言代码(如C、C++、Java等)转换为低级机器语言代码(如汇编语言或机器码)的过程。编译器是执行这一转换过程的程序。编译的主要目的是将人类可读的高级语言代码转换为汇编代码。将.i文件翻译为.s文件的过程叫做编译。
作用:
语法检查:编译器会对源代码进行语法检查,确保其符合语言规范,否则会提示错误信息。
词法分析:编译器会将源代码分解成词法单元(token),如标识符、关键字、运算符等。
语义分析:编译器会对词法单元进行分析,确保代码在语义上是正确的,如类型匹配、变量声明等。
优化:编译器可能会对源代码进行优化,以提高程序的性能和效率。优化可能涉及到减少计算、减少内存占用、减少指令数等方面。
生成目标代码:在分析和优化完毕后,编译器会生成对应的目标代码,这通常是汇编语言。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
图3.2.1
3.3 Hello的编译结果解析
3.3.1 伪指令部分
图3.3.1
图3.3.1是汇编程序开头的伪指令,这些伪指令大多以‘.’开头,记录文件相关信息,主要用于直到后续汇编器和链接器的工作。.file表明了源文件名称,.text意为代码段,.section .rodata意为只读代码段,.align说明了对齐方式为8字节对齐,.string为程序用到的字符串,.global为全局变量,.type说明了main是一个函数。
3.3.2 数据
1.数字常量
图3.3.2.1
通过立即数$9来实现i<10。
2.字符串常量
图3.3.2.2
这部分代码用于输出错误信息"用法: Hello 学号 姓名 手机号 秒数!\n"。leaq .LC0(%rip), %rdi将字符串常量的地址加载到%rdi寄存器中,然后通过puts函数输出。
这部分代码用于循环中输出"Hello %s %s %s\n"。先通过一系列movq和addq指令将参数argv[1]、argv[2]和argv[3]的地址加载到寄存器中,然后通过leaq .LC1(%rip), %rdi将格式字符串的地址加载到%rdi,再通过call printf@PLT调用printf函数输出格式化字符串。打印字符串常量时,编译器将语句翻译为先将字符串存放的地址存入寄存器%rdi,再进行打印。
3.局部变量
hello中涉及局部变量操作的地方主要是源程序中的第12行int i;。
图3.3.2.3
这一句中在hello.s中对应的语句主要是如图3.3.2.4中选出的语句。在这一句中,栈顶指针rep向上移动32个字节,在栈中为局部变量i保留了空间。
图3.3.2.4
可知局部变量i存放在栈中-4(%rbp)的位置。
传入参数char* argv[]存放在栈中:
图3.3.2.5
根据框选出的这些代码可以得出:argc存储在-20(%rbp)位置,argv存储在-32(%rbp)位置。%edi寄存器中保存argc,%rsi寄存器中保存argv。因此参数argc和argv都保存在栈帧的特定位置。
在循环体中,通过一系列movq和addq指令访问argv中的各个参数。首先,将argv的基地址存入%rax寄存器。然后,通过addq $8, %rax操作获取argv[1]的地址,接着使用movq (%rax), %rax指令将argv[1]的内容加载到%rax寄存器。最后,将%rax寄存器的值移动到%rsi寄存器中,以便进一步处理。
类似地,访问argv[2]的过程也类似,只是使用偏移量为$16来获取地址,并将内容加载到%rdx寄存器中。
对于argv[3],使用偏移量为$24获取其内容,并将其加载到%rcx寄存器。
最后处理argv[4],使用偏移量为$32获取其内容,将其加载到%rax寄存器中,随后将其移动到%rdi寄存器。接下来调用atoi函数将其转换为整数。
3.3.3赋值操作:
赋值操作的实现在汇编语言中主要是通过mov指令实现的。如下图,将i赋值为0.
图3.3.2.6
3.3.4 关系指令与跳转
hello.c程序中主要用到的关系指令有两个:== 和 <= ,!=的条件判断在本程序中使用je。判断argc是否与5相等,相等就跳转到.L2。
图 3.3.2.7
而<=使用jle。如下图,比较-4(%rbp)与9,如果前者<=9,跳转到.L4。
上述两个案例均用于控制转移操作中,分别用于if语句和for语句中,从中也可看出,if语句和for语句的控制转移是通过比较指令cmp和跳转控制指令实现的。
3.3.5 函数调用与参数传递
在64位编译模式下,汇编程序的参数传递机制涉及使用寄存器和栈。前六个参数通过寄存器传递,分别使用rsi、rdi、rdx、rcx、r8、r9寄存器。如果参数超过六个,则额外的参数会被压入栈中。
函数调用的过程通常包括以下步骤:首先,压入返回地址,即函数调用后继续执行的位置。接着,可能会压入函数参数。然后,压入当前函数的基地址指针(rbp)。最后,为了保存现场,可能会保存在函数中使用的寄存器内容。函数执行完毕后,这些步骤会逆序执行:先从栈中弹出保存的寄存器内容,接着恢复rbp,然后弹出函数参数,最后跳回到保存的返回地址处继续执行。
3.4 本章小结
本章详细介绍了编译过程中,将hello.i文件通过编译器转换为汇编语言的过程,并对汇编语言程序进行了解析,根据C语言的各种类型和操作进行分析。汇编语言相比于高级编程语言更直接地操作计算机内存和寄存器等硬件部分,其统一的格式有助于统一不同高级编程语言生成的程序结构,为后续的汇编和链接阶段提供准备。
第4章 汇编
4.1 汇编的概念与作用
概念:
汇编是指汇编器(as)将汇编语言程序hello.s翻译为机器语言指令,并将这些指令打包成可重定位目标程序的格式,将结果保存在目标文件hello.o文件中的过程。
作用:
计算机只能识别处理机器指令程序,汇编过程将汇编语言程序翻译为了机器指令,进一步向计算机能够执行操作的形式迈进,便于计算机直接进行分析处理。同时为之后的链接阶段打下基础。通过将汇编代码翻译成机器指令并生成目标文件,汇编器为链接器提供了必要的输入文件。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
图4.2.1
4.3 可重定位目标elf格式
指令:readelf -a hello.o >hello.o.elf
图4.3.1
4.3.1 ELF头
hello.o的ELF格式以一个16字节(一共40字节)的Magic序列开始,描述了生成该文件的系统的字大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包含ELF头的大小,程序头部表的大小,目标文件的类型,机器类型,节头部表的文件偏移,以及节头部表中条目的大小和数量。
图4.3.2
4.3.2 节头目表:
ELF文件格式中的节头部表描述了目标文件中不同节的类型、地址、大小、偏移等信息,以及可以对各部分进行的操作权限。
图4.3.3 节头目表
4.3.3 重定位信息
图4.3.4 重定位节
在分析hello程序时,并未找到.rel.data节,这表明程序中没有需要重定位的已初始化数据。
重定位条目的作用是告诉链接器如何修改引用,以便将目标文件合并为可执行文件。重定位条目存储在.rel.text节中,用于代码,以及在.rel.data节中,用于初始化数据。
重定位条目包含以下信息:offset表示需要修改的引用在节中的偏移量;symbol指定被引用的目标符号;type指示链接器如何调整新的引用;addend是一个有符号常数,某些重定位类型使用addend来调整引用的值。
具体类型包括:
1.RX8664_PC32:用于32位PC相对地址的引用重定位。PC相对地址是相对于当前运行时的程序计数器(PC)值的偏移量。
2.RX8664_32:用于32位绝对地址的引用重定位。CPU直接使用指令中编码的32位值作为有效地址,无需修改。
这些重定位类型帮助CPU在执行时计算正确的地址,如call指令的目标地址。
4.3.4符号表
图4.3.5 符号表
在可重定位模块m的符号表中,存放了三种不同类型的符号:
1.全局符号:由模块m定义并且可以被其他模块引用。
2.外部符号:由其他模块定义,但在模块m中被引用。
3.局部符号:仅由模块m定义和引用,不被其他模块访问。
符号表中的字段解释如下:
4.name:指向以NULL结尾的字符串,表示符号的名字,存储在字符串中的字节偏移位置。
5.value:对于可重定位模块,表示符号相对于定义目标节起始位置的偏移量。
6.size:符号的大小,以字节为单位。
7.type:标识符号是数据还是函数。
8.binding:指示符号是本地的(局部符号)还是全局的(全局符号)。
此外,符号表还包含各个节的条目以及对应的原始源文件路径名的条目。
4.4 Hello.o的结果解析
objdump -d -r hello.o >hello.o.asm
图4.4.1
对比hello.s内容与hello.o.asm:
图4.4.2 hello.o.asm
图4.4.3 hello.s
可知,汇编指令的代码几乎与反汇编代码完全对应。反汇编后的代码基于机器代码和汇编指令的原始基础,同时包括左侧的机器代码。在跳转指令中,反汇编代码展示了直接跳转到相应地址的情况,而hello.s中的跳转则是以.Lx代码块为单位进行的跳转。常量操作数在反汇编代码中通常以十六进制显示,而hello.s中则更常见地使用十进制数字。此外,hello.s中大量使用以“.”开头的伪指令,而在反汇编代码中则不见这种情况。hello.s中包含.type ,.rodata,.file,.section ,.align(对齐) 等信息,反汇编文件hello.o .asm中只有代码段.text的相关内容。在hello.s中,跳转指令使用段名进行分支转移,指定跳转位置。而在hello.o的反汇编代码中,每个段都被赋予了具体的地址,跳转指令则使用相应的绝对或相对地址来表示跳转位置。
4.5 本章小结
本章详细探讨了汇编语言的定义及其功能,重点强调了如何使用汇编器(as)将汇编语言转换为二进制机器指令,并将其打包成可重定位目标文件(hello.o)。使用readelf工具分析了hello.o文件的ELF格式,包括ELF头部、节头表、重定位节和符号表等基本信息。
随后,利用objdump工具对hello.o进行反汇编,生成了hello.o.asm文本文件,并分析了反汇编代码与原始汇编语言程序hello.s之间的对应关系。具体分析包括数字进制的表示、字符串常量的引用、分支跳转和函数调用等方面。
第5章 链接
5.1 链接的概念与作用
链接是指将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可以被加载到内存并执行。
链接的主要作用就是使得分离编译成为可能,从而不需要将一个大型的应用程序组织成一个巨大的源文件,而是可以将其分解成更小的、更好管理的模块,可以独立的修改和编译这些模块
5.2 在Ubuntu下链接的命令
指令:ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2/usr/lib/x86_64-linux-g
nu/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
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
使用readelf -s hwllo >hello.elf文件
1. ELF头
2. hello的节头部表
|
图5.3-4 hello的节头部表
3. hello的程序头表
ELF可执行文件的连续的片被映射到了连续的内存段,从而很容易被加载到内存中。程序头部表描述了这种映射关系。
|
Offset代表目标文件中的偏移,VirtAdrr代表虚拟内存的地址,PhysAddr代表物理地址的内存,FileSiz代表目标文件中的段大小,MemSiz代表内存中的段大小,flags代表运行时的访问权限,align代表对齐要求。
可执行目标文件的格式类似于可重定位目标文件的格式类似于可重定位目标文件的格式。ELF头描述文件的总体格式。它还包括程序的入口点,也就是程序运行时要执行的第一条指令的地址。.text、.rodata和.data节与可重定位目标文件的节是相似的,除了这些节已经被重定位到它们最终的运行内存izhi以外。.init节定义了一个小函数,叫做_init,程序的初始化代码会调用它。因为可执行文件是完全链接的(已被重定位),所以不再需要rel节。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
由下图可以得到,虚拟地址空间的起始位置是0x400000
由上图5.3-3可以得知,.interp段的起始地址为0x4002e0,在edb中查找地址得到如下图的结果:
由上图5.3-4可以得到,.text的起始地址为0x4010f0,在edb中查询地址可以得到如下图的结果:
由上图5.1-5可以得到,.rodata的起始地址为0x402000,在edb中查询地址可以得到如下图的结果:
5.5 链接的重定位过程分析
Hello的反汇编文件:
hello: 文件格式 elf64-x86-64
Disassembly of section .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__@Base>
40100f: 48 85 c0 test %rax,%rax
401012: 74 02 je 401016 <_init+0x16>
401014: ff d0 call *%rax
401016: 48 83 c4 08 add $0x8,%rsp
40101a: c3 ret
Disassembly of section .plt:
0000000000401020 <.plt>:
401020: ff 35 e2 2f 00 00 push 0x2fe2(%rip) # 404008 <_GLOBAL_OFFSET_TABLE_+0x8>
401026: f2 ff 25 e3 2f 00 00 bnd jmp *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 push $0x0
401039: f2 e9 e1 ff ff ff bnd jmp 401020 <_init+0x20>
40103f: 90 nop
401040: f3 0f 1e fa endbr64
401044: 68 01 00 00 00 push $0x1
401049: f2 e9 d1 ff ff ff bnd jmp 401020 <_init+0x20>
40104f: 90 nop
401050: f3 0f 1e fa endbr64
401054: 68 02 00 00 00 push $0x2
401059: f2 e9 c1 ff ff ff bnd jmp 401020 <_init+0x20>
40105f: 90 nop
401060: f3 0f 1e fa endbr64
401064: 68 03 00 00 00 push $0x3
401069: f2 e9 b1 ff ff ff bnd jmp 401020 <_init+0x20>
40106f: 90 nop
401070: f3 0f 1e fa endbr64
401074: 68 04 00 00 00 push $0x4
401079: f2 e9 a1 ff ff ff bnd jmp 401020 <_init+0x20>
40107f: 90 nop
401080: f3 0f 1e fa endbr64
401084: 68 05 00 00 00 push $0x5
401089: f2 e9 91 ff ff ff bnd jmp 401020 <_init+0x20>
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 jmp *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 jmp *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 jmp *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 jmp *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 jmp *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 jmp *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: 45 31 c0 xor %r8d,%r8d
401106: 31 c9 xor %ecx,%ecx
401108: 48 c7 c7 25 11 40 00 mov $0x401125,%rdi
40110f: ff 15 db 2e 00 00 call *0x2edb(%rip) # 403ff0 <__libc_start_main@GLIBC_2.34>
401115: f4 hlt
401116: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1)
40111d: 00 00 00
0000000000401120 <_dl_relocate_static_pie>:
401120: f3 0f 1e fa endbr64
401124: c3 ret
0000000000401125 <main>:
401125: f3 0f 1e fa endbr64
401129: 55 push %rbp
40112a: 48 89 e5 mov %rsp,%rbp
40112d: 48 83 ec 20 sub $0x20,%rsp
401131: 89 7d ec mov %edi,-0x14(%rbp)
401134: 48 89 75 e0 mov %rsi,-0x20(%rbp)
401138: 83 7d ec 05 cmpl $0x5,-0x14(%rbp)
40113c: 74 19 je 401157 <main+0x32>
40113e: 48 8d 05 c3 0e 00 00 lea 0xec3(%rip),%rax # 402008 <_IO_stdin_used+0x8>
401145: 48 89 c7 mov %rax,%rdi
401148: e8 43 ff ff ff call 401090 <puts@plt>
40114d: bf 01 00 00 00 mov $0x1,%edi
401152: e8 79 ff ff ff call 4010d0 <exit@plt>
401157: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
40115e: eb 56 jmp 4011b6 <main+0x91>
401160: 48 8b 45 e0 mov -0x20(%rbp),%rax
401164: 48 83 c0 18 add $0x18,%rax
401168: 48 8b 08 mov (%rax),%rcx
40116b: 48 8b 45 e0 mov -0x20(%rbp),%rax
40116f: 48 83 c0 10 add $0x10,%rax
401173: 48 8b 10 mov (%rax),%rdx
401176: 48 8b 45 e0 mov -0x20(%rbp),%rax
40117a: 48 83 c0 08 add $0x8,%rax
40117e: 48 8b 00 mov (%rax),%rax
401181: 48 89 c6 mov %rax,%rsi
401184: 48 8d 05 ad 0e 00 00 lea 0xead(%rip),%rax # 402038 <_IO_stdin_used+0x38>
40118b: 48 89 c7 mov %rax,%rdi
40118e: b8 00 00 00 00 mov $0x0,%eax
401193: e8 08 ff ff ff call 4010a0 <printf@plt>
401198: 48 8b 45 e0 mov -0x20(%rbp),%rax
40119c: 48 83 c0 20 add $0x20,%rax
4011a0: 48 8b 00 mov (%rax),%rax
4011a3: 48 89 c7 mov %rax,%rdi
4011a6: e8 15 ff ff ff call 4010c0 <atoi@plt>
4011ab: 89 c7 mov %eax,%edi
4011ad: e8 2e ff ff ff call 4010e0 <sleep@plt>
4011b2: 83 45 fc 01 addl $0x1,-0x4(%rbp)
4011b6: 83 7d fc 09 cmpl $0x9,-0x4(%rbp)
4011ba: 7e a4 jle 401160 <main+0x3b>
4011bc: e8 ef fe ff ff call 4010b0 <getchar@plt>
4011c1: b8 00 00 00 00 mov $0x0,%eax
4011c6: c9 leave
4011c7: c3 ret
Disassembly of section .fini:
00000000004011c8 <_fini>:
4011c8: f3 0f 1e fa endbr64
4011cc: 48 83 ec 08 sub $0x8,%rsp
4011d0: 48 83 c4 08 add $0x8,%rsp
4011d4: c3 ret
与hello.o.asm相比,hello.asm在几个关键方面有显著区别:
1.虚拟地址差异:hello.o.asm中的反汇编代码虚拟地址从0开始,而hello.asm中的反汇编代码虚拟地址从0x400000开始。这是因为hello.o在链接之前使用相对地址,而hello在链接后获得了绝对地址。
2.反汇编节的不同:hello.o.asm只包含了.text节,其中仅包含main函数的反汇编代码。而hello.asm在main函数之前增加了链接过程中加入的其他函数和数据,导致额外的节如.init, .plt, .plt.sec等,这些节的反汇编代码也会出现在文件中。
3.跳转指令的区别:在hello.o.asm中,跳转指令后跟的是汇编代码块前的标签。而在hello.asm中,跳转指令后跟的是具体的地址,但相对地址没有改变。
重定位过程可以分为两个主要步骤:重定位与符号定义。在重定位与符号定义步骤中,链接器将所有相同类型的节合并为一个新的聚合节,并为输入模块中定义的每个节和符号分配运行时内存地址。完成这一步骤后,程序中的每条指令都有了唯一的运行时内存地址。第二个步骤是重定位节的符号引用。在这一步骤中,链接器修改了代码节和数据节中对每个符号的引用,以使其指向正确的运行时地址。这个过程的实现依赖于重定位条目的类型,其中包括相对引用和绝对引用两种方式,以完成对每个符号的引用的调整。
5.6 hello的执行流程
调用edb运行可执行程序hello1,右键点击analyze here可以得到hello1的函数列表信息,如下图所示
5.7 Hello的动态链接分析
当程序调用一个由共享库定义的函数时,编译器无法预知函数在运行时的具体地址,因为这个定义函数的共享模块可以加载到内存中的任何位置。因此,编译系统采用延迟绑定的方法,将函数地址的绑定推迟到第一次调用该函数时进行。
延迟绑定过程中使用了两个数据结构:全局偏移表(Global Offset Table,GOT)和过程链接表(Procedure Linkage Table,PLT)。PLT是一个数组,每个条目占据16字节空间。PLT[0]是一个特殊条目,用于跳转到动态链接器。每个被程序调用的库函数都有自己的PLT条目,每个条目负责调用一个具体的函数。GOT是一个数组,每个条目是8字节地址。GOT和PLT结合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时所需的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应一个被调用的函数,其地址需要在运行时解析。每个GOT条目都有一个对应的PLT条目。
在第一次调用某个函数时,程序不直接跳转到函数地址,而是通过调用该函数对应的PLT条目。PLT条目的第一条指令通过GOT进行间接跳转,初始时每个GOT条目指向其对应PLT条目的第二条指令,这个间接跳转只是简单将控制传回到PLT条目的下一条指令。接着,函数的ID被压入栈中,PLT条目跳转到PLT[0],PLT[0]通过GOT[1]间接将动态链接器的参数压入栈中,然后通过GOT[2]间接跳转进入动态链接器。动态链接器利用栈上的两个条目来确定函数的运行时位置,并将控制传递给函数。
在后续调用中,程序可以直接通过PLT条目跳转到函数,而无需再通过GOT的跳转。
由图5.3-5查看hello的ELF文件,得到GOT运行时的地址0x403ff0和PLT运行时的地址0x404000。之后在程序调用dl_init之前,先查看0x404000位置的内容:
在dl_init调用前,对于每一条PIC函数调用,初始时每个GOT条目都指向了PLT条目的第二条指令。
在调用dl_init后,可以看到对应内容发生了变化。
分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
本章节简要介绍了链接的相关过程。首先,概述了链接的定义和其在软件开发中的重要作用,并介绍了在Ubuntu系统下执行链接的指令。接着,详细分析了可执行目标文件hello的ELF格式,使用edb调试工具查看了虚拟地址空间及几个节的内容。然后,根据重定位条目深入分析了重定位的过程,并利用edb调试工具研究了程序中各个子程序的执行流程。最后,利用edb调试工具通过对虚拟内存的检索,深入分析了动态链接的执行过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程为程序提供了独立的逻辑控制流,使得程序可以表现出独占使用处理器的特性。此外,进程还为程序提供了私有的地址空间,确保程序可以享有独占使用内存系统的能力。
6.2 简述壳Shell-bash的作用与处理流程
Shell作为一种交互式程序,其主要功能是代表用户来运行其他程序。
处理流程如下:首先对命令行参数进行求值,检查是否为空。如果不为空,则判断第一个命令行参数是否是一个内置命令。如果是内置命令,则直接执行相应的操作;否则,Shell会检查是否是一个可执行的应用程序。接着,Shell会在预定义的路径中搜索这些应用程序。如果用户输入的命令不是内置命令且在路径中找不到对应的可执行文件,Shell将会显示一条错误信息。若成功找到命令,Shell会将其解析为系统调用,并将其传递给Linux内核执行。
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的运行中的子进程。子进程几乎与父进程完全相同,但在某些方面有所不同。子进程获得了父进程的用户级虚拟地址空间的独立副本,包括代码段、数据段、堆、共享库和用户栈。此外,子进程也继承了父进程打开的文件描述符的副本,因此它能够读写父进程打开的任何文件。父进程和子进程的主要区别在于它们具有不同的进程标识号(PID)。
fork函数在程序中被调用一次,但会导致两次返回。首先,在父进程中,fork返回新创建的子进程的PID;其次,在子进程中,fork返回0。通过检查fork函数的返回值,程序可以确定当前是在父进程还是在子进程中执行。父进程和子进程是并发运行的独立进程,内核以任意方式交替执行它们的逻辑控制流。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。
execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。execve函数一次调用而从不返回。
6.5 Hello的进程执行
1.逻辑控制流是指程序计数器(PC)值的一个序列,这些值依次对应于可执行目标文件中的指令或者动态链接到运行时共享对象中的指令。
2.时间分片指的是多道程序设计中,进程在处理器上轮流执行的过程。每个进程按照其控制流的一部分依次执行,然后可能被抢占(暂时挂起),让其他进程执行。当多个进程在时间上重叠执行时,称为并发流,它们同时运行。
3.用户模式和内核模式是处理器在运行过程中的两种权限级别。为了保护操作系统内核的安全性,处理器通过控制寄存器中的模式位来区分这两种模式。当模式位被设置时,进程处于内核模式,否则处于用户模式。
初始时,程序代码运行在用户模式下。当发生中断、异常或系统调用时,进程从用户模式切换到内核模式。处理器将控制传递给异常处理程序,在处理程序执行期间保持在内核模式。处理程序完成后,处理器将控制返回到应用程序代码,并将模式从内核模式切换回用户模式。
|
4. 在进程的执行过程中,内核有时会决定抢占当前正在执行的进程,并重新开始之前被中断的进程。这种决策称为调度,由操作系统内核中的调度器来处理。在执行抢占时,需要进行上下文切换操作,这一过程中,当前进程的上下文被保存下来,同时之前被抢占进程的上下文被恢复,然后控制权转移到被恢复的进程。
6.6 hello的异常与信号处理
1.乱打字
在程序运行时乱打字,不影响程序的正常运行。随著printf的调用保留在了输出结果中。
2.按下ctrl-Z
|
Ctrl-Z是挂起当前作业,但不会回收。
Ps:命令可以看到hello进程并没有被回收。
jobs可以查到hello的后台job id是1。
fg将hello调到前台重新开始运行,结合参考上图,输出次数与10次循环总数吻合
Ctrl-C命令内核向前台发送SIGINT信号,终止了前台作业。
6.7本章小结
本章节详细描述了hello进程的执行流程,涵盖了进程、shell、fork、execve等相关概念。随后,从逻辑控制流、时间分片、用户模式和内核模式、以及上下文切换等角度深入分析了进程的执行过程。在运行时,还对各种形式的命令和异常进行了尝试,每种信号都有其特定的处理机制。对于不同的shell命令,hello进程展现出不同的响应方式。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1.逻辑地址
编译后程序在汇编代码中的地址,用于指定操作数或指令的位置。逻辑地址由段标识符和段内偏移量组成,表示为段标识符:段内偏移量。
2.物理地址
存储器中以字节为单位存储信息,每个字节单元对应唯一的存储器地址,也称为实际地址或绝对地址。物理地址对应系统中实际的内存字节。
3.虚拟地址
CPU在启动保护模式后,程序运行在虚拟地址空间中。虚拟地址空间包含所有可能的地址,例如在64位机器上,虚拟地址空间可达到2^64种可能。
4.线性地址
逻辑地址到物理地址转换的中间层。在分段机制中,逻辑地址是段内的偏移地址,加上段基地址可以得到线性地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在 Intel 平台下,逻辑地址由 selector:offset 形式组成,其中 selector 是 CS 寄存器的值,offset 是 EIP 寄存器的值。通过使用 selector 去全局描述符表 (GDT) 中获取段基址 (segment base address),然后加上 offset (段内偏移),可以得到线性地址。这个过程称为段式内存管理。
逻辑地址包含段标识符和段内偏移量。段标识符是一个16位长的字段(即段选择符),通过段选择符的前13位可以直接在段描述符表中找到对应的段描述符,这个描述符用于描述一个段。
全局的段描述符存储在 GDT 中,而一些局部的段描述符可能存储在局部段描述符表 (LDT) 中。
通过给定完整的逻辑地址(段选择符 + 段内偏移地址),可以根据段选择符中的 T1 位来确定是从 GDT 还是 LDT 中获取段描述符。然后根据相应的寄存器内容获取段的地址和大小。查找段描述符时,只需使用段选择符的前13位即可在描述符表中找到对应的段描述符,从而获取其基地址。最终,将段基地址与段内偏移相加即可得到线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址到物理地址的转换通过分页机制完成,这种机制将虚拟内存划分为大小固定的虚拟页。为了确定虚拟页是否存在于DRAM中,并找到其对应的物理页位置,系统使用一个页表。页表由页表条目(PTE)组成的数组构成,每个PTE包含一个有效位和一个n位地址字段。有效位的状态表示虚拟页是否被缓存在DRAM中。地址翻译过程中,使用一个页表基址寄存器指向当前页表。一个n位的虚拟地址分为两部分:p位的虚拟页面偏移和n-p位的虚拟页号。
如果虚拟页已经被缓存,即发生页面命中,内存管理单元(MMU)利用虚拟页号(VPN)选择适当的PTE,然后将该PTE中的物理页号与虚拟页面偏移组合,从而得到物理地址。如果虚拟页未被缓存,发生缺页,触发缺页异常,控制传递到缺页异常处理程序。缺页处理程序确定物理内存中的牺牲页,然后判断是否因为牺牲页被修改而需要将其调出内存。完成牺牲页的替换后,新页面被调入并更新内存中的PTE。最后,控制返回到原始进程,重新发出引起缺页的虚拟地址给MMU,这次会命中虚拟页已缓存的情况。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是MMU中的一种缓存,用于存储PTE,被称为快表。它是一个小而高速的虚拟地址缓存,每个条目存储一个PTE。四级页表是一种多级结构,用于压缩页表以便有效地管理大量内存。在地址转换过程中,虚拟地址的页号VPN被分为k个部分,每个VPNi是第i级页表的索引。对于1 <= j <= k-1,它们指向下一级页表。第k级页表的每个PTE包含一个物理页面的PPN或者磁盘块的地址。要得到物理地址,MMU需要访问k个PTE以确定PPN。Intel Core i7使用四级页表结构,每个VPNi有9位。当TLB未命中时,36位的VPN被划分为VPN1、VPN2、VPN3、VPN4,每个VPNi作为一个页表的偏移量。CR3寄存器包含L1页表的物理地址,VPN1提供L1 PTE的偏移量,该PTE包含L2页表的基址。VPN2提供到L2页表中某个PTE的偏移量,依此类推。最终的L4 PTE包含所需的物理页号,与虚拟地址中的VPO结合即得到物理地址。
TLB加速了地址转换,而多级页表压缩了页表以便有效管理大量存储。在将虚拟地址翻译为物理地址的过程中,MMU首先通过VPN向TLB请求对应的PTE。如果命中,直接跳过后续步骤;否则,MMU生成PTE地址,并从高速主存请求PTE。高速缓存或主存将PTE返回给MMU。如果PTE的有效位为0,表明发生缺页,MMU触发缺页异常。缺页处理程序确定物理内存中的牺牲页(若页面已修改,则写回磁盘)。然后,缺页处理程序将新页面调入内存并更新PTE。最后,缺页处理程序返回到原始进程,并重新执行导致缺页的指令。
7.5 三级Cache支持下的物理内存访问
根据内存地址的组索引,如果要查找的是数据,则向L1数据缓存(d-cache)的相应组进行查询;如果是指令,则向L1指令缓存(i-cache)的相应组进行查询。对于每一行,在L1对应组中,比较标记位是否匹配,并检查有效位是否为1,以确定是否命中。如果命中,则获取偏移量并取出相应的字节;如果未命中,则继续向下一级缓存查找,直至访问内存。
7.6 hello进程fork时的内存映射
当调用fork函数时,内核为新进程创建各种数据结构,并为其分配一个独特的PID。为了创建新进程的虚拟内存,它复制了当前进程的mm_struct、区域结构和页表的完整副本,并将两个进程中的每个页面标记为只读。此外,它将两个进程中的每个区域结构标记为私有的写时复制。
当fork在新进程中返回时,新进程的虚拟内存状态与调用fork时父进程的虚拟内存状态相同。后续如果任一进程进行写操作,写时复制机制将会创建新的页面,从而为每个进程维护了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数用于在当前进程中加载并运行包含在可执行目标文件a.out中的程序,从而有效地替换当前程序。加载和运行a.out包括以下几个步骤:
- 清除已存在的用户区域: 删除进程虚拟地址空间中已存在的用户区域;
- 映射私有区域: 为新程序的代码、数据、bss和栈区域创建新的区域结构。这些区域是私有的,并采用写时复制策略。代码和数据区域映射到a.out文件中的.text和.data区域。bss区域被映射到一个匿名文件,大小由a.out文件指定。栈和堆区域也被请求为零的二进制,初始长度为零。
- 映射共享区域: 如果a.out程序链接了共享对象(例如标准C库libc.so),这些对象会动态链接到该程序,并映射到用户虚拟地址空间的共享区域中。
- 设置程序计数器: execve最后一步是设置当前进程上下文的程序计数器,使其指向代码区域的入口点。。
7.8 缺页故障与缺页中断处理
缺页故障指的是当一个虚拟页未被缓存在DRAM中,即DRAM缓存未命中。当CPU访问一个页表条目中的字时,发现该页表条目未被缓存到DRAM中(通过检查有效位为0来判断),这会触发缺页异常。
处理缺页中断的过程如下:当缺页异常发生时,操作系统会调用缺页异常处理程序。该程序会选择一个要替换的牺牲页,如果这个牺牲页在DRAM中已被修改,会将其写回磁盘。接着,被引用的虚拟页会被复制到这个牺牲页的位置,并更新页表条目。处理程序完成后返回,引导原来因缺页而中断的指令重新执行。此时,被请求的虚拟页已经被缓存在主存中,地址翻译硬件可以正常处理页命中。
7.9动态存储分配管理
动态内存分配使用动态内存分配器,它负责管理进程的虚拟内存区域,通常称为堆。分配器维护了一组不同大小的块集合,每个块是一个连续的虚拟内存片段,可以是已分配或空闲状态。已分配的块明确保留供应用程序使用,而空闲块则可用于分配。空闲块在分配前保持空闲状态,已分配块在释放前保持已分配状态。释放操作可以由应用程序显式执行,也可以由内存分配器自动隐式执行。
动态内存分配器通常有两种基本风格:
1.显式分配器:要求应用程序显式释放任何已分配的块。
2.隐式分配器(或垃圾收集器):要求分配器监测已分配块何时不再被应用程序使用,并在不再使用时自动释放这些块。垃圾收集是指自动释放未使用的已分配块的过程。分配器有两种基本风格:
7.10本章小结
本章主要探讨了存储管理的相关内容。首先介绍了四种不同的存储器地址空间,接着详细阐述了Intel处理器中逻辑地址到线性地址的转换机制,讲解了段式管理的实现方式。随后,概述了线性地址到物理地址的转换过程,即页式管理,并详细描述了用于加速地址转换的TLB(快表)和压缩页表的四级页表。接着简要说明了通过TLB和四级页表实现虚拟地址到物理地址的映射机制。随后,提及了在三级Cache支持下的物理内存访问流程,从内存映射的角度回顾了fork和execve函数。然后简单介绍了缺页故障及其处理机制,最后讨论了动态存储分配管理的方法和机制。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
在Linux系统中,每个文件被视为一个包含m个字节序列的实体。所有的I/O设备都被抽象为文件,因此所有的输入输出操作都以文件的读取和写入方式进行。这种将设备映射为文件的机制,使得Linux内核能够提供一个简单且基础的应用程序接口,通常称为Unix I/O。这种设计使得所有的输入和输出操作都可以以统一且一致的方式执行。
8.2 简述Unix IO接口及其函数
1.Unix文件操作接口:
2.打开文件: 应用程序通过内核打开文件,以表明其要访问一个I/O设备。内核返回一个小的非负整数,称为文件描述符,用于标识该文件并记录相关信息。应用程序只需记住此描述符。
3.I/O设备: 所有的I/O设备都被抽象为文件。应用程序通过文件描述符进行所有操作,内核管理文件的状态和位置。
4.改变文件位置: 对于每个打开的文件,内核维护一个文件位置指针k,初始为0,表示从文件开头的字节偏移量。应用程序可以显式地通过seek函数改变当前文件位置k。
5.读写文件: 读操作从文件中复制n个字节到内存中,从当前文件位置k开始,然后增加k到k+n。如果当前位置k大于等于文件大小m,会触发EOF。写操作将n个字节从内存复制到文件中的当前位置k,并更新k。
6.关闭文件: 应用程序完成文件访问后通知内核关闭文件。内核释放文件打开时分配的资源,并将文件描述符返回到可用的描述符池中。当进程终止时,内核会自动关闭所有打开的文件并释放资源。
7.Unix I/O函数:
8.int open(char *filename, int flags, mode_t mode);
进程通过调用open函数打开现有文件或创建新文件:
9.函数将filename转换为文件描述符,并返回最小的未使用描述符。
10.flags参数指定访问方式:ORDONLY(只读)、OWRONLY(只写)、O_RDWR(读写)。
11.mode参数指定新文件的权限位。
12.int close(int fd);
进程调用close函数关闭打开的文件,fd为文件描述符。
13.ssizet read(int fd, void *buf, sizet n);
从描述符为fd的当前文件位置读取最多n个字节到内存位置buf。
14.返回值-1表示错误,0表示EOF,否则返回实际传输的字节数。
15.ssizet write(int fd, const void *buf, sizet n);
将内存位置buf的最多n个字节写入描述符为fd的当前文件位置。
16.返回值为写入的字节数,-1表示错误。
这些函数和接口提供了一种统一且一致的方式来管理文件和设备的输入输出操作,使得应用程序可以方便地与各种I/O设备进行交互,而无需关心具体的底层实现细节。
8.3 printf的实现分析
1.printf函数的函数体:
va_list是一个字符指针的重定义,(char*)((&fmt) + 4 )是第一个参数,这与栈的结构有关,*fmt存放在栈中,后续的字符型指针也都存在栈中,而指针大小位四个字节,所以+4得到第一个参数。之后调用了vsprintf函数,其函数体如下图所示:
vsprintf主要用于格式化,接受输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar的源代码如下:
getchar函数调用了read函数,通过系统调用read读取存储在键盘缓冲区的ASCII码,直到读到回车符才返回。如果键盘缓冲区本来就有内容,read函数会直接返回缓冲区中的第一个元素,而不会进行实际的系统调用。
在处理异步异常(如键盘中断)时,键盘中断处理子程序会接收按键扫描码并将其转换成ASCII码,然后将ASCII码保存到系统的键盘缓冲区中。
getchar等函数调用read系统函数,通过系统调用来读取键盘输入的ASCII码,直到接收到回车键才返回。
8.5本章小结
本章重点介绍了IO管理机制,首先概述了将IO设备抽象为文件的概念。接着详细介绍了Unix操作系统中的IO设备管理方法,即Unix IO接口。在介绍Unix IO接口后,列举了相关的Unix IO函数。在此基础上,进一步分析了printf和getchar函数的工作原理。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
1.创作源代码:
程序员使用高级语言在编辑器中编写了名为 hello.c 的文件,并将其保存在内存中,这是程序 "hello" 的创作开始。
2.预处理阶段:
通过预处理器对 hello.c 文件进行处理,生成了经过修改的文本文件 hello.i。在此阶段,注释被移除,宏替换完成,使得程序 "hello" 开始显露出初步形态。
3.编译过程:
编译器将 hello.i 翻译成汇编语言,生成了 hello.s 文件。此时,程序 "hello" 从高级语言转换为更接近机器语言的汇编代码。
4.汇编转换:
汇编器将 hello.s 转换成机器可读的可重定位目标文件 hello.o。在这一步骤中,程序 "hello" 已经具备了运行的基础结构。
5.链接器操作:
链接器将 hello.o 与其他必要的目标文件链接在一起,生成了最终的可执行文件 hello。此时,程序 "hello" 已准备就绪,可以执行。
6.程序执行阶段:
在 shell 中运行 hello 程序时,操作系统创建了一个子进程,并通过 execve 系统调用将该程序转变为独立运行的进程,使得程序 "hello" 获得了自主运行的能力。
7.进程执行过程:
程序 "hello" 在获取到 CPU 时间片后开始执行其自身的逻辑,通过三级缓存访问内存,将虚拟地址映射为物理地址,以实现高效的内存操作。
8.信号处理:
在运行过程中,程序 "hello" 可以接收到各种信号(如终止信号、挂起信号等),并根据不同的信号进行相应的处理,展示了程序对异常控制流的管理能力。
9.程序终止:
当使用 kill 命令终止 hello 进程时,操作系统回收子进程的资源,标志着程序 "hello" 的生命周期结束。
感悟:
通过“hello”程序的整个生命周期,包括创建、编译、链接、执行和终止的过程,我深入理解了计算机系统的内部运作方式。逐步拆解和实验每个阶段,使我对代码如何在底层执行和交互有了更深刻的认识。这样的学习经历不仅增强了我们编写更高效、可靠代码的能力,也拓展了我们对计算机系统整体的理解。
计算机系统的实现远比我最初想象的复杂。在我们日常使用中,每个看似简单的功能背后都蕴含着复杂的知识和原理。‘hello’程序的生命周期告诉我们,计算机科学领域并非顺利而自然,每一个表面轻松的操作都建立在前人巧妙的思想基础之上。要精通计算机科学,需要深入钻研,追求卓越。
附件
hello.c C语言源文件
hello.i 预处理产生的文件
hello.s 编译产生的汇编代码文件
hello.o 汇编产生的可重定位目标文件
hello.o.asm hello.o反汇编生成的文本文件
hello.asm hello反汇编生成的文本文件
hello 链接产生的可执行目标文件
hello.elf hello文件的ELF格式
hello.o.elf hello.o文件的ELF格式
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Randal E. Bryant;David R.O’Hallaron. 深入理解计算机系统.背景:机械工业出版社,2016.7
[3] 长路漫漫2021 Shell和Bash的区别和联系 http://t.csdnimg.cn/VXp25.