程序人生-Hello’s P2P
目 录
第1章 概述
1.1 Hello简介
1.2 环境与工具
1.3 中间结果
1.4 本章小结
第2章 预处理
2.1 预处理的概念与作用
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
2.4 本章小结
第3章 编译
3.1 编译的概念与作用
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.4 本章小结
第4章 汇编
4.1 汇编的概念与作用
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
4.4 Hello.o的结果解析
4.5 本章小结
第5章 链接
5.1 链接的概念与作用
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
5.4 hello的虚拟地址空间
5.5 链接的重定位过程分析
5.6 hello的执行流程
5.7 Hello的动态链接分析
5.8 本章小结
第6章 hello进程管理
6.1 进程的概念与作用
6.2 简述壳Shell-bash的作用与处理流程
6.3 Hello的fork进程创建过程
6.4 Hello的execve过程
6.5 Hello的进程执行
6.6 hello的异常与信号处理
6.7本章小结
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.2 Intel逻辑地址到线性地址的变换-段式管理
7.3 Hello的线性地址到物理地址的变换-页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
7.5 三级Cache支持下的物理内存访问
7.6 hello进程fork时的内存映射
7.7 hello进程execve时的内存映射
7.8 缺页故障与缺页中断处理
7.9动态存储分配管理
7.10本章小结
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
8.2 简述Unix IO接口及其函数
8.3 printf的实现分析
8.4 getchar的实现分析
8.5本章小结
结论
附件
参考文献
第1章 概述
1.1 Hello简介
Hello程序的生命周期始于一个用高级C语言编写的源文件hello.c。在Linux系统中,该程序需要经历一系列转换步骤才能执行:首先,预处理器(cpp)对源代码进行宏展开和头文件包含,生成预处理文件hello.i;随后,编译器(cc1)将预处理后的代码转换为汇编语言文件hello.s;接着,汇编器(as)把汇编代码翻译成机器指令,并打包为可重定位目标文件hello.o;最后,链接器将hello.o与标准C库(如printf等函数)合并,生成最终的可执行文件hello。当用户在shell中输入运行命令时,系统通过fork()创建子进程,并调用execve()加载hello程序,使其从静态的程序(program)转变为动态运行的进程(process),完成从程序到进程(P2P)的转变。
在Shell中,fork()创建子进程后,execve加载可执行程序hello,为其分配虚拟内存空间,并在程序启动时将其载入物理内存,交由CPU执行。CPU为hello分配时间片,通过取指、译码、执行等流水线操作运行程序。内存管理单元(MMU)与CPU协同工作,利用L1、L2、L3多级缓存和TLB页表机制高效访问物理内存数据。同时,I/O系统按照程序指令执行输入/输出操作。当程序运行结束时,父进程回收子进程资源,内核将其从系统中彻底清除。至此,hello从“进程(Process)”回归“空无(Zero)”,完成020(From Zero to Zero)的生命周期。
1.2 环境与工具
泰山服务器、DELL服务器
虚拟机:VirtualBox/Vmware 11以上 + Ubuntu 18.04 LTS 64位/优麒麟 64位 以上;
软件平台:Linux、C/C++
调试工具:gdb、CodeBlocks(限虚拟机下使用,服务下不能使用)
1.3 中间结果
文件名
作用
hello.c
原C语言文件
hello.i
预处理产生文件
hello.s
编译产生文件
hello.o
汇编产生文件
hello.out
链接产生可执行文件
hello
链接产生可执行文件
hello_o_elf.txt
查看hello.o的elf格式对应的文本文件
hello_o_asm.txt
查看hello.o的反汇编对应的文本文件
hello_elf.txt
查看hello的elf格式对应的文本文件
hello_asm.txt
查看hello的反汇编对应的文本文件
1.4 本章小结
本章主要介绍了hello程序的P2P的过程。
同时介绍了此次大作业的硬件环境、软件环境以及开发工具。
还列出了为完成本次大作业,生成的中间结果文件的名字以及文件的作用
第2章 预处理
2.1 预处理的概念与作用
由预处理器(cpp)负责执行,它通过解析源代码中以`#`开头的指令来修改原始程序。例如,hello.c中的#include 指令会指示预处理器读取标准库头文件stdio.h的内容,并将其直接插入到程序文本的相应位置。经过这一系列处理后,原始的C程序被转换为一个经过宏展开和头文件合并的新版本,通常以.i作为文件扩展名,形成预处理后的中间文件hello.i。
预处理阶段主要处理C源文件中以`#`开头的预处理指令,主要包含以下五种类型:
1.宏定义(#define):执行符号替换,将定义的宏名替换为指定的常量或字符串;
2.文件包含(#include):将指定头文件的内容完整插入到当前文件中;
3.条件编译(#if/#elif/#else/#endif):根据条件判断决定编译哪些代码段;
4. 注释处理:移除源代码中的所有注释内容;
5.特殊指令(#error/#warning等):提供编译时的错误提示和警告控制功能。
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
Hello.i中不再有注释部分,并且插入了大量的代码。其中有在源程序中的库函数的stdio.h,unistd.h,以及stdlib.h的代码,它们都是被直接插入了程序文本中。
但在hello.i中依旧有与hello.c的main函数相同的代码
2.4 本章小结
本章系统阐述了C语言预处理的核心机制及其五大功能:宏定义替换、文件包含处理、条件编译控制、注释清除以及特殊指令处理。通过实验验证,在Ubuntu环境下使用预处理器将hello.c转换为hello.i文件。
对比分析预处理前后的文件差异发现:预处理过程会完整保留除注释和预处理指令外的原始代码内容。具体表现为:
1)所有注释内容被完全移除;
2)头文件及其嵌套包含的文件内容被递归展开并直接插入对应位置;
3)宏定义被实际值替换;
4)条件编译指令根据条件保留有效代码段。
第3章 编译
3.1 编译的概念与作用
编译是指将便于人编写、阅读、维护的高级计算机语言所写作的源代码程序,翻译为计算机能解读、运行的低阶机器语言的程序。在这里指编译器(cc1)将文本文件hello.i翻译成汇编语言程序hello.s的过程。
编译的作用是将高级计算机语言所写作的源代码程序翻译为汇编语言程序,在这个过程中,会进行以词法分析、语法分析、语义分析来生成汇编语言程序,且编译器可能在这个过程中根据编译选项对程序进行一些适当的优化。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1 数据
1. 数字常量
在hello.c中出现的数字常量在hello.s中都有相应的对应。编译器将数字常量以立即数的形式进行处理。
将整型数argc与5比较,数字常量5在hello.s中以立即数$5的形式出现。
在循环的判断条件中出现数字常量,用立即数表示。注意到由于循环的判断条件为i<10,编译器将其翻译为小于等于立即数$9。
2. 字符串常量
在hello.c文件中,有两个字符串,如下图所示:
编译器对这两句字符串进行处理时,将这两个字符串放入内存中的 .rodata节常量区中。
打印字符串常量时,编译器将语句翻译为先将字符串存放的地址存入寄存器%rdi。
3. 局部变量
编译器一般将局部变量存放在寄存器中或者栈中,存在寄存器中时可以看作是寄存器别名,节省从内存中读取数据的时间。
传入参数int argc存放在寄存器%edi中。
传入参数char *argv[]存放在栈中,其中首地址存放在栈中-32(%rbp)的位置,argv[1]地址为-32(%rbp)+$8,argv[2]地址为-32(%rbp)+$16,argv[3]地址为-32(%rbp)+$24,argv[4]地址为-32(%rbp)+$32。
局部变量int argc存放在栈中-20(%rbp)的位置。
3.3.2 赋值
编译器将赋值的操作主编为对相应的寄存器或栈进行赋值。
在hello. c文件中,有对i的赋值如下:
经过编译器cc1的编译,在hello.s文件中,该语句转变为:
栈中-4(%rbp)的位置存放的是局部变量i,因此是将i赋值为0。
3.3.3 类型转换
Hello.c文件中, argv[3]的类型为字符型,经过函数atoi()转换为整型。经过编译器编译后,步骤变为首先将argv[3]从栈中取出,赋值给%rdi,通过调用call atoi@PLT指令调用atoi函数,最终转换为整型数,存放在%eax中。
3.3.4 算术操作
Hello.c文件中,算术运算有for循环中的i++:
经过编译器后,被翻译为:
栈中-4(%rbp)的位置存放的是局部变量i,每次执行这条指令,实现的是i自增1。
3.3.5 关系操作
对于关系操作,编译器一般会将关系操作翻译为cmp语句。在源文件hello.c中,有两处关系操作:
比较argc与4是否相等
经过编译,在hello.s文件中,该关系比较实现为:
Cmpl $5,-20(%rbp)
在3.3.1中,已经知道栈中-20(%rbp)的位置存放的是argc,因此这条指令就是在判断5与argc的关系。
比较i与10的大小,i >= 10时跳出循环,在hello.c中的实现为:
经过编译,在hello.s文件中,该关系比较实现为:Cmpl $9,-4(%rbp)
jle .L4
在3.3.1中,已经知道栈中-4(%rbp)的位置存放的是i,并且这里将i<10替换为判断i<=9,意义与原C语言程序相同。
3.3.6 数组/指针/结构操作
编译器对源代码中数组的操作往往翻译为对地址的加减操作,在hello.c的源代码中,存在对数组argv[]的访问:
经过编译器的翻译,在hello.s的文件中,对数组argv的访问变为:
首地址存放在栈中-32(%rbp)的位置,argv[1]地址为-32(%rbp)+$8,argv[2]地址为-32(%rbp)+$16,argv[3]地址为-32(%rbp)+$24,argv[4]地址为-32(%rbp)+$32,即进行了地址的加减操作以访问数组。
3.3.7 控制转移
控制转移是指C语言源文件中的选择分支、循环结构等经过编译器的翻译,产生一些跳转的语句,编译器cc1编译后的文件hello.s中,控制转移有三处:
比较argc是否等于5,如果相等,跳转.L2,否则顺序执行。
无条件跳转,将i初始化为0后,无条件跳转至循环中。比较i是否小于等于9(小于10),如果小于等于9,则跳转至.L4,即满足循环条件,继续循环;如果大于9,则顺序执行,跳出循环。
3.3.8 函数调用
函数调用一般会进行参数传递和返回值。在hello.s中,一共有六次函数调用。
调用puts()函数。首先将调用函数所需要的参数,即需要打印的字符串常量的地址存放在寄存器%rdi中,然后执行call puts@PLT指令打印字符串。对应的C语言源程序如下:
可以看到,在C语言源程序中执行该命令用的是printf函数,由于打印的是一个单纯的字符串,因此编译器对它进行了优化,改用puts函数进行打印。
调用exit()函数,参数为1。对应的C语言源程序如下图所示:
在hello.s中,首先进行参数的准备。将立即数1放入寄存器%edi中,然后执行call exit@PLT指令调用exit函数。
调用含其他参数的printf()函数。对应的C语言源程序如下图所示:
在hello.s中,首先进行参数的准备。将argv[2]放入寄存器%rdx中,将argv[1]放入寄存器%rsi中,将字符串\"Hello %s %s %s\\n\"放入寄存器%rdi中,然后执行call printf@PLT指令进行打印。
在hello.s中,首先进行参数的准备。将argv[2]放入寄存器%rdx中,将argv[1]放入寄存器%rsi中,将字符串\"Hello %s %s %s\\n\"放入寄存器%rdi中,然后执行call printf@PLT指令进行打印。
在hello.s中,首先进行参数的准备。将argv[2]放入寄存器%rdx中,将argv[1]放入寄存器%rsi中,将字符串\"Hello %s %s %s\\n\"放入寄存器%rdi中,然后执行call printf@PLT指令进行打印。
在hello.s中,首先进行参数的准备。将argv[4]放入寄存器%rdi中,然后执行call atoi@PLT指令调用atoi函数。
调用sleep()函数,参数为atoi(argv[4])的返回值,以下是C语言原程序中的函数调用:
在hello.s中,首先进行参数的准备。将atoi(argv[3])的返回值放入寄存器%edi中,然后执行call sleep@PLT指令调用sleep函数。
调用getchar()函数,没有参数,以下是C语言原程序中的函数调用
在hello.s中,直接执行call getchar@PLT指令进行函数的调用。
3.4 本章小结
本章系统阐述了编译阶段的核心概念与功能,重点分析了编译器如何将预处理后的高级语言代码转换为等价的汇编语言表示,并通过优化算法提升代码执行效率。基于Ubuntu平台,我们通过实验将hello.i文件编译生成了对应的hello.s汇编文件。
通过对比分析C语言源代码与生成的汇编代码,可以清晰地观察到不同类型的数据和操作在汇编层面的具体实现方式:
1)数据类型的转换处理:
数字常量直接编码为立即数
字符串常量存储在数据段
局部变量通过栈空间分配实现
2)操作指令的转换机制:
基本运算(算术/关系运算)映射为CPU指令
控制流(条件/循环)转换为跳转指令
函数调用遵循特定的调用约定
复杂数据结构(数组/指针)通过地址计算实现
第4章 汇编
4.1 汇编的概念与作用
汇编是指将汇编语言程序经过编译器转化为二进制的机器语言指令,并把这些指令打包成可重定位目标程序的格式,并保存在目标文件.o中。在这里指汇编器器将文本文件hello.s转换为可重定位目标程序hello.o的过程。
汇编的作用是把汇编语言翻译成机器语言,用二进制码0、1代替汇编语言中的符号,即让它成为机器可以直接识别的程序。最后把这些指令打包成可重定位目标程序的格式,并保存在目标文件.o中。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
目标文件ELF格式解析:
1.ELF头结构:
起始16字节包含关键系统信息:
字长(32/64位)
字节序(大端/小端)
后续部分提供链接关键参数:
LF头本身尺寸
目标文件类型(可重定位/可执行等)
目标平台架构
节头部表位置偏移量
节头部表条目规格(单个条目大小、总条目数)
2. 功能说明:
该头部结构为链接器提供了完整的文件解析蓝图,使其能够准确定位并处理后续的各个节区内容。通过标准化的格式设计,确保不同平台下的工具都能正确解读目标文件结构。
ELF文件格式中的节头部表描述了目标文件中不同节的类型、地址、大小、偏移等信息,以及可以对各部分进行的操作权限。
ELF文件格式中的重定位节包含两个部分:.rela.text节与.rela.eh_frame节
.rela.text节包含.text节中的位置的列表,含有该.text中所需要进行重定位操作的信息,当链接器(ld)将目标文件与其他文件由进行结合时,需要修改这些位置
.rela.eh_frame节包含了对en_frame节的重定位信息。
ELF文件格式中的符号表中存放了程序中所定义和引用的的全局变量以及函数的信息。
4.4 Hello.o的结果解析
4.4.1 数值表示方式差异
汇编代码(hello.s):采用十进制表示操作数
反汇编代码:使用十六进制显示
机器底层:实际以二进制形式存储
4.4.2 字符串引用处理
hello.s:通过段名+%rip相对寻址
hello.o:使用0+%rip临时占位
(原因:可重定位目标文件需待链接阶段完成最终地址重定位)
4.4.3 段处理与跳转指令
hello.s:
显式标注各段名称
跳转目标使用段名标识
hello.o反汇编:
为每个段分配明确地址
跳转指令使用具体地址
4.4.4 函数调用机制
hello.s:
call指令直接引用函数名
hello.o反汇编:
call指令后跟下条指令地址
操作数暂用0占位
4.5 本章小结
本章详细解析了汇编阶段的核心机制与功能实现,重点阐述了汇编器(as)如何将汇编语言程序转换为可重定位的二进制目标文件。基于Ubuntu开发环境,通过实验将hello.s汇编文件转换为hello.o可重定位目标文件。
4.1 汇编处理过程
1)指令转换:将助记符形式的汇编指令转换为二进制机器码
2)数据编码:处理各类常量与变量的存储表示
3)格式封装:按照ELF标准打包为可重定位目标文件格式
4.2 ELF格式解析
使用readelf工具对hello.o进行结构分析,重点研究了:
ELF头部:包含文件标识与架构信息
节区表:描述各节区的布局属性
重定位节:记录需要链接时修正的地址引用
符号表:维护全局符号的定位信息
4.3 反汇编对比分析
通过objdump工具反汇编hello.o,从以下维度与原始汇编代码进行对比:
1)数值表示体系:十进制→十六进制→二进制的转换关系
2)地址引用方式:符号引用到地址占位的转换
3)控制流实现:标签到实际地址的映射过程
4)函数调用机制:符号调用到相对地址引用的转换
第5章 链接
5.1 链接的概念与作用
链接是将程序中的代码段、数据段等不同模块进行整合的过程,主要实现三个层面的功能整合:
1. 编译时链接(静态链接)
在源代码转换为机器码阶段完成
将多个目标文件(.o)合并为可执行文件
典型工具:GNU ld链接器
2. 加载时链接(动态链接基础)
程序载入内存时由加载器完成
处理共享库的地址重定位
涉及动态段(.dynamic)解析
3. 运行时链接(完全动态链接)
由应用程序主动控制
支持插件式功能扩展
通过dlopen/dlsym等API实现
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
5.4 hello的虚拟地址空间
- 在edb的symbol窗口,可以查看各段对应的名称以及各段的起始位置与结束的位置,与5.3中所展示出来的elf格式展示出来的相对应。
- Data Dump是从地址0x400000开始的,并且该处有ELF的标识,可以判断从可执行文件加载的信息(只读代码段,读/写段)是从地址0x400000处开始的。可以从程序头处读取相关信息。
- PDHR起始位置为0x400040 大小为0x230。
- INTERP起始位置为0x400270 大小为0x1c。
- DYNAMIC起始位置为0x403e10 大小为0x1e06. .GNU_RELRO起始位置为0x403e50 大小为0x1b0
5.5 链接的重定位过程分析
打开反汇编代码的文本文件,查看两个文件代码量,发现hello_o_asm.txt只有59行,而hello_asm.txt有255行。
在hello.o的反汇编程序中,只有main函数,没有调用的函数段;经过链接过程后,原来调用的C标准库中的代码都被插入了代码中,并且每个函数都被分配了各自的虚拟地址。
在hello.o的反汇编程序中, main函数中的所有语句前面的地址都是从main函数开始从0开始依次递增的,而不是虚拟地址;经过链接后,每一条语句都被分配了虚拟地址。
字符串常量寻址方式
hello.o反汇编:
使用\"0 + %rip\"临时占位
原因:尚未分配虚拟内存地址
hello反汇编:
使用\"实际偏移量 + %rip\"精确定位
实现:基于重定位后的虚拟地址空间
2.函数调用机制
hello.o反汇编:
all指令使用\"下条指令地址\"占位
特征:函数地址未确定
hello反汇编:
call指令直接使用函数虚拟地址
实现:通过PLT/GOT完成动态链接
3.跳转指令处理
hello.o反汇编:
使用从0开始的相对地址
hello反汇编:
使用虚拟内存绝对地址
关键技术:
重定位条目(relocation entry)修正
地址对齐保证
1. 符号解析阶段
建立全局符号表
解决符号引用关系
处理强弱符号规则
2. 重定位阶段
地址空间分配:
代码段(.text)起始地址:0x400000
数据段(.data)内存布局
引用修正:
绝对地址重定位(R_X86_64_32)
PC相对地址重定位(R_X86_64_PC32)
动态链接处理:
生成过程链接表(PLT)
全局偏移表(GOT)填充
hello程序链接实现
1. 虚拟地址空间构建:
代码段:0x400000-0x401000
数据段:0x601000-0x602000
2. 关键符号重定位:
main函数入口地址
printf等库函数调用
字符串常量存储位置
3. 执行时内存映射:
通过ELF程序头加载
页面对齐处理
动态链接器介入
5.6 hello的执行流程
从加载hello到_start,到call main,以及程序终止的所有过程如下:
_dl_start 地址:0x7f894f9badf0
_dl_init 地址:0x0x7f894f9cac10
_start 地址:0x401090
_libc_start_main 地址:0x7fce59403ab0
_cxa_atexit 地址:0x7f38b81b9430
_libc_csu_init 地址:0x4005c0
_setjmp 地址:0x7f38b81b4c10
_sigsetjmp 地址:0x7efd8eb79b70
_sigjmp_save 地址:0x7efd8eb79bd0
main 地址:0x401176
(argc!=3时
puts 地址:0x401030
exit 地址:0x401070
此时输出窗口打印出“用法: Hello 学号 姓名 秒数!”,程序终止。)
print 地址:0x401040
sleep 地址:0x401080 (以上两个在循环体中执行8次)
此时窗口打印8行“Hello 2023112344 陈俊 2”
getchar 地址:0x4004d0
等待用户输入回车,输入回车后:
_dl_runtime_resolve_xsave 地址:0x7f5852241680
_dl_fixup 地址:0x7f5852239df0
_uflow 地址:0x7f593a9a10d0
exit 地址:0x7f889f672120
程序终止。
5.7 Hello的动态链接分析
首先,通过readelf找到.got.plt节在地址为0x410fe8的地方开始,大小为0x48。因此,结束地址为0x4264015,这两个地址之间部分便是.got.plt的内容。
在edb中的Data Dump中找到这个地址,观察.got.plt节的,发现在dl_init前后,.got.plt的第8到15个字节发生了变化。
5.8 本章小结
本章主要阐述了链接的基本概念与功能,其核心作用是将预编译生成的目标文件(如hello.o)与链接库整合为可执行文件(如hello)。具体以Ubuntu系统为例,演示了通过链接器(ld)将hello.o转换为hello的过程。
通过分析hello的ELF格式,使用readelf工具详细展示了各节头信息,包括起始地址与大小等关键数据。借助edb调试器观察虚拟地址空间布局,验证了各节名称与对应虚拟地址区域的映射关系。
对hello进行反汇编后,将其结果与hello.o的反汇编代码对比,发现链接后代码规模显著扩展:新增了C标准库函数代码,所有指令均获得虚拟地址,且字符串引用、函数调用及跳转指令的地址均被替换为实际虚拟地址。
深入解析了链接的两个关键阶段——符号解析与重定位机制。通过edb逐步跟踪hello的执行流程,完整呈现了从程序加载、_start入口到main函数调用直至终止的全过程,并记录了各子程序的调用链与地址信息。
最后探讨了动态链接特性,重点分析.got.plt节在dl_init前后的内容变化,揭示了延迟绑定机制对动态链接过程的影响。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念
进程是正在执行的程序的实例。每个运行的程序都处于某个进程的上下文中,该上下文包含了程序正确执行所需的全部状态信息,包括:
内存中的代码和数据
运行时栈
通用寄存器的值
程序计数器(PC)
环境变量
打开的文件描述符
6.1.2 进程的作用
进程为程序提供了两个关键抽象:
独立的逻辑控制流:使程序看似独占处理器资源,按顺序执行指令。
私有的地址空间:使程序看似独占内存系统,拥有自己的代码、数据和栈。
此外,进程机制使得CPU能够高效地划分时间片,实现多进程并发执行,提高系统资源利用率。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 Shell(Bash)的作用
Shell(如Bash)为用户提供命令行交互界面,负责接收用户输入的命令,并执行读取-解析-执行的循环过程:
读取:获取用户输入的命令行字符串。
解析:分析命令结构,确定执行方式。
执行:运行对应程序或内置命令。
该循环持续运行,直至用户主动退出Shell,从而实现用户与操作系统的交互。
6.2.2 Shell(Bash)的处理流程
Shell的执行流程遵循以下步骤:
提示与等待:显示提示符(如$),等待用户输入命令。
命令解析:
若为内置命令(如cd、echo),直接执行。
若为可执行程序,则:
通过fork()创建子进程。
使用execve()加载并运行目标程序。
通过waitpid()等待子进程结束并回收资源(前台任务)。
若为后台任务(以&结尾),则Shell直接返回,继续接收新命令。
循环执行:重复上述过程,直至用户退出Shell。
6.3 Hello的fork进程创建过程
Fork 进程创建过程
fork() 系统调用允许父进程创建一个全新的子进程。调用 fork() 后,系统会生成一个与父进程几乎完全相同的子进程副本,包括:
内存空间:子进程获得父进程虚拟地址空间的独立副本,包含:代码段数据段 堆空间 共享库 用户栈
文件描述符:子进程继承父进程所有已打开的文件描述符,具有相同的读写权限。
子进程拥有独立于父进程的PID
fork() 调用一次但返回两次:
父进程获得子进程的PID
子进程获得返回值0
以 hello 程序为例,当在 shell 中输入:
./hello 2023112344 陈俊 2 3
shell 会通过 fork() 创建一个子进程来执行 hello 程序,该子进程成为 shell 进程的子进程。
6.4 Hello的execve过程
Execve 函数在 fork 创建子进程后被调用,用于在当前进程上下文中加载并运行 hello 程序。该函数首先通过 _start 初始化新的栈空间(清零),然后将控制权交给 main 函数,同时传递参数列表和环境变量列表。execve 是一个\"一次调用,永不返回\"的函数,仅在发生错误时才会返回到调用程序。当 hello 可执行文件被加载后,启动代码会完成栈设置、将磁盘中的程序代码和数据载入内存等工作,最后跳转到程序入口点执行第一条指令,从而将控制权正式移交给新程序的 main 函数开始执行。
6.5 Hello的进程执行
进程调度与执行过程分析(以hello程序为例)
在操作系统管理下,hello程序的执行过程展现了典型的进程调度机制。当hello开始运行时,系统为其分配CPU时间片,此时处理器物理控制流被划分为多个逻辑控制流。在多进程环境下,这些逻辑流通过时间片轮转实现并发执行,每个进程获得的时间段即为时间片。进程在用户态执行期间,系统会保存其上下文信息(包括寄存器状态、程序计数器等)。
当发生异常或系统中断时,内核将触发以下处理流程:
当前进程(如hello)被置为休眠状态
执行用户态到核心态的转换
在内核空间完成上下文保存与切换
调度器选择新进程投入运行
以hello程序中的sleep调用为例:
进程主动进入休眠状态
内核执行完整的上下文切换
控制权移交其他就绪进程
休眠结束后,系统恢复hello的上下文
进程从休眠点继续执行
在调用getchar()时,进程经历更复杂的状态转换:
用户态到核心态的转换(系统调用)
可能引发I/O等待导致的进程切换
内核维护进程状态直至I/O就绪
最终通过上下文恢复机制返回hello进程
程序终止阶段,return语句触发:
资源回收系统调用
进程控制块清理
父进程接收终止状态信息
进程空间完全释放
整个过程充分展现了现代操作系统通过时间片轮转、上下文切换和状态转换实现的并发执行机制,以及用户态与核心态之间的协同工作原理。
6.6 hello的异常与信号处理
(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.6.1 正常运行
正常执行时,hello每隔两秒打印一行“Hello 2023112344 陈俊 2”,进入循环,共打印10次。打印完毕后,调用getchar()函数,等待用户输入回车后程序终止。Shell回收hello子进程,继续等待用户输入指令。
6.6.2 不停乱按
当在程序执行过程中随机乱按时,按下的字符串会直接显示,但不会干扰程序的运行,由于在乱按过程中没有输入回车,所以在最后一行hello的字符串打印完毕后,需要敲一个回车才能退出程序。
6.6.3 按回车
在hello执行过程中敲回车时,会首先再打印的过程中显示换行,一个回车显示一排换行。在打印完毕最后一行字符串后,由于输入的回车依然存在于stdin中,所以在调用getchar()函数时,会读取stdin中的回车,因此无需再敲回车键,便能终止程序。
程序终止后,发现shell中出现2个空行,这是因为在程序的执行过程中,敲了3下回车键,因此都留在stdin中,getchar()只接收了其中的第一个回车,由于在程序终止后没有清空stdin,剩余的回车保留在其中。当shell继续运行时,遇到回车便开始处理,但单独的回车相当于一个空行,被shell忽略,读入但不执行任何操作,因此留下了2个空行。
6.6.4 按Ctrl-z
在程序执行过程中按Ctrl-z,产生中断异常,发送信号SIGSTP,这时hello的父进程shell会接收到信号SIGSTP并运行信号处理程序。
最终的结果是hello被挂起,并打印相关信息。
1. 输入ps
Ctrl-z之后,在shell命令行中输入ps,打印出各进程的pid,其中包括被挂起的hello。
2. 输入jobs
Ctrl-z之后,在shell命令行中输入jobs,打印出被挂起的hello的jid及标识。
3. 输入pstree -p
Ctrl-z之后,在shell命令行中输入pstree -p,查看进程树之间的关系,同时输出对应的进程pid。在进程树中找到hello(2476),发现hello的父进程是zsh(2233),从祖先进程到hello的树为:systemed(1)→systemed(1477) →gnome-terminal-(2226) →zsh(2233)→hello(2476)
4. 输入fg
Ctrl-z之后,在shell命令行中输入fg,被挂起在后台的hello进程被重新调到前台执行,打印出剩余部分,按回车后终止程序
5. 输入kill
Ctrl-z之后,输入kill,该进程被杀死
6.6.5 按Ctrl-c
运行hello时按Ctrl-C,会导致断异常,从而内核产生信号SIGINT,发送给hello的父进程,父进程收到它后,向子进程发生SIGKILL来强制终止子进程hello并回收它。这时再运行ps,可以发现并没有进程hello,可以说明他已经被终止并回收了。
6.7本章小结
本章系统阐述了进程管理的核心机制及其在hello程序执行中的具体体现。首先从理论层面解析了进程的本质——作为程序执行的实例,操作系统通过为每个进程维护独立的逻辑控制流和私有地址空间,实现CPU资源的虚拟化和高效并行处理。
在交互层面,重点分析了Shell(以bash为例)的工作原理。作为用户与操作系统的桥梁,Shell采用\"读取-解析-执行\"的循环机制:通过命令行界面接收用户输入,解析命令语义后,针对内置命令直接执行,外部程序则通过进程创建机制运行,直至用户主动终止会话。
针对hello程序的执行过程,详细剖析了进程创建与加载的关键步骤:
通过fork()系统调用创建子进程副本
使用execve()完成程序加载和上下文初始化
结合时间片轮转机制实现多进程并发执行
用户态与核心态的动态转换机制
上下文信息的保存与恢复过程
特别探讨了程序运行时的异常处理机制。通过模拟用户交互场景(包括常规输入、Ctrl-Z挂起、Ctrl-C终止等操作),结合进程监控命令(ps/jobs/pstree等),深入分析了:
信号产生与传递机制
进程状态转换过程(运行/挂起/终止)
前后台进程管理策略
进程终止与资源回收流程
这些分析不仅揭示了操作系统底层的工作机制,也展现了用户操作与系统响应的完整交互链条。
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1 逻辑地址机制
逻辑地址是程序生成的段相关偏移地址,由段基址和段偏移量两部分组成。在CPU保护模式下,需通过地址转换机制才能获取有效内存地址。以hello程序为例,其反汇编代码中的地址均为逻辑地址,必须结合段基址才能完成有效寻址。
7.1.2 线性地址转换
线性地址作为逻辑地址到物理地址转换的中间层,是地址空间的连续整数表示。其计算方式为段基址与逻辑地址中的偏移量之和。在hello程序的反汇编分析中,通过将代码偏移地址与对应段基址相加,即可获得准确的线性地址。
7.1.3 虚拟地址特性
虚拟地址是程序访问存储器时使用的逻辑地址表示。CPU通过生成虚拟地址访问主存,该地址需转换为物理地址后才能实际访存。在Linux系统中,虚拟地址与线性地址具有数值等价性。分析hello的ELF格式可见,程序头中的VirtAddr字段即为各节的虚拟地址,其数值等于对应的线性地址。
7.1.4 物理地址实现
物理地址是主存存储单元的唯一标识,系统将主存组织为连续的字节单元数组。在hello执行过程中,程序使用的虚拟地址经过MMU地址翻译后转换为物理地址,处理器最终通过这些物理地址完成实际的数据存取操作。这一转换过程确保了程序地址空间与物理存储的隔离与映射。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由2部分组成:段选择符和段内偏移量。
段选择符的构成由一个16位长的字段组成;其中前13位是索引号,用来确定当前使用的段描述符在描述符表中的位置;后面3位表示一些硬件细节,包含TI与RPL:TI选择全局(GDT)或局部描述符表(LDT),RPL选择内核态与用户态。
根据段选择符,首先判断应该选择全局描述符表(GDT,TI=0)还是局部描述符表(LDT,TI=1);然后根据GDT与LDT所对应的寄存器,得到地址和大小,获得段描述符表;接着,查看段选择符的前13位,通过索引在段描述符表中找到对应的段描述符;从而可以得到Base字段,即开始位置的线性地址。
将开始位置的线性地址与段内偏移量相加,就能得到相应的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
现代计算机系统采用多级页表机制实现虚拟地址到物理地址的高效转换。线性地址(虚拟地址)由虚拟页号(VPN)和虚拟页偏移量(VPO)组成,其中VPN用于索引页表,VPO则作为页内偏移。页表作为页表条目(PTE)的数组,每个PTE记录着虚拟页到物理页的映射关系,包含有效位、物理页号和保护位等关键信息。
地址转换过程始于CPU获取存储在CR3寄存器中的页目录基地址。转换时,处理器首先利用线性地址的高位字段作为索引逐级查询页目录和页表,最终定位到目标物理页帧。将获得的物理页基地址与线性地址中的页偏移量相加,即得到最终的物理地址。为提升转换效率,现代处理器采用转换后备缓冲器(TLB)缓存常用地址映射,当TLB命中时可直接获取转换结果,否则需要查询内存中的页表结构。
当访问的页面不在物理内存时,会触发缺页异常。此时操作系统介入处理,从磁盘交换区加载所需页面到内存,并更新页表条目。这种按需调页的机制有效实现了虚拟内存管理,既扩展了可用内存空间,又通过局部性原理保证了访问效率。整个过程涉及硬件地址转换单元与操作系统内存管理模块的紧密配合,共同构建了现代计算机系统的虚拟内存体系。
7.4 TLB与四级页表支持下的VA到PA的变换
现代计算机系统采用TLB(转换后备缓冲器)来加速虚拟地址到物理地址的转换过程。TLB作为专门用于缓存页表条目的高速缓存,具有较高的相联度,其工作原理是将虚拟页号(VPN)划分为标记位(TLBT)和索引位(TLBI),其中索引位用于选择TLB组,标记位用于匹配具体条目。当TLB命中时,CPU生成的虚拟地址通过MMU直接从TLB获取对应的PTE,快速完成地址转换并访问高速缓存或主存。
在TLB未命中的情况下,系统采用四级页表结构进行地址转换。虚拟地址被划分为四个VPN字段和一个VPO字段,每个VPN字段作为对应级别页表的索引。转换过程从CR3寄存器指向的L1页表开始,逐级向下查询:L1页表的PTE指向L2页表基址,L2指向L3,L3指向L4,最终从L4页表的PTE中获取物理页号(PPN),与VPO组合形成物理地址。这种多级页表结构虽然增加了转换步骤,但通过仅在需要时创建下级页表的方式,显著节省了内存空间,特别是对于稀疏的地址空间尤为有效。
整个地址转换机制体现了现代计算机系统在时空效率上的精妙平衡:TLB通过空间局部性原理加速常见地址转换,而多级页表则通过按需分配的策略优化内存使用。硬件与操作系统的协同设计使得虚拟内存管理既保持了高效性,又具备了良好的可扩展性,能够适应从嵌入式设备到大型服务器等各种计算环境的需求。
7.5 三级Cache支持下的物理内存访问
当MMU完成虚拟地址到物理地址的转换后,会将该物理地址(PA)发送给L1缓存进行数据查询。L1缓存首先对物理地址进行解码,将其分解为三个关键字段:缓存偏移(CO)用于定位块内数据位置,缓存组索引(CI)确定目标缓存组,缓存标记(CT)用于标识匹配的缓存行。在采用8路组相联结构的L1缓存中,系统会并行检查选定组内的全部8个缓存行,通过比较各行的标记位(CT)并验证有效位状态来检测命中情况。
若缓存命中,则根据CO字段从匹配的缓存行中提取目标数据,经由MMU返回给CPU执行单元。若未命中,则按照存储层次结构依次查询L2、L3缓存直至主存。在数据逐级查询过程中,一旦在较低层级存储中发现所需数据,系统会触发缓存填充机制:首先检查目标组是否存在空闲块,若有则直接载入;当组内所有缓存行均被占用时,则采用最近最少使用(LRU)算法选择替换候选行。这个多级查询与替换过程严格遵循包含性原则,确保高层级缓存中的数据总是下层缓存的子集,从而维持存储系统的一致性。整个缓存访问机制通过并行匹配和智能替换策略,在纳秒级时间内完成数据定位,为CPU提供持续高效的数据供给。
7.6 hello进程fork时的内存映射
fork()系统调用实现进程创建的核心机制在于写时复制(Copy-on-Write)技术。当父进程调用fork()时,内核执行以下精密操作:首先构建子进程的进程描述符,分配唯一的PID标识;然后复制父进程的虚拟内存管理结构,包括mm_struct内存描述符、虚拟内存区域(VMA)链表以及页表结构。值得注意的是,此时父子进程共享相同的物理内存页,但内核会将这些共享页面的权限设置为只读,并将所有VMA区域标记为私有写时复制属性。
在fork()返回后,子进程拥有与父进程完全一致的虚拟地址空间布局,且两者共享相同的物理内存内容。这种精妙的设计使得fork()操作无需立即复制大量物理内存,极大提升了进程创建效率。当任一进程(父进程或子进程)尝试执行写操作时,会触发页保护异常,此时内核的写时复制机制才真正介入:分配新的物理页面,复制原始内容,更新发起写操作进程的页表项使其指向新页面,并恢复可写权限。通过这种延迟复制策略,既确保了进程间内存空间的隔离性,又避免了不必要的内存复制开销,实现了高效安全的进程复制语义。
7.7 hello进程execve时的内存映射
execve函数实现程序加载的核心过程可分为四个关键阶段:
内存空间重构阶段:
清除当前进程用户空间的所有现有内存映射
建立新的私有内存区域结构,包括:
代码段(.text)和数据段(.data)直接映射到hello文件的对应节区
.bss段创建为初始化为零的匿名文件映射
栈和堆区域初始化为零长度的可扩展空间
所有新建区域均采用写时复制(COW)机制
动态链接处理阶段:
解析hello程序的动态依赖关系
将所需的共享库映射到进程地址空间的共享区域
完成重定位符号解析
执行上下文初始化阶段:
将程序计数器(PC)设置为.text段的入口地址(通常是_start符号)
初始化寄存器状态和栈帧结构
设置参数和环境变量指针
延迟加载机制:
采用按需分页策略,初始仅建立虚拟地址映射
实际代码和数据页在首次访问时由缺页异常处理程序动态加载
共享库实施延迟绑定技术
该过程通过虚拟内存管理和文件映射技术,实现了高效的程序加载机制,在保持进程执行环境隔离性的同时,最大程度减少了物理内存的即时占用。
7.8 缺页故障与缺页中断处理
虚拟内存系统中,当CPU访问的虚拟页(如VP3)未缓存在物理内存(DRAM)时,就会触发缺页异常(Page Fault)。这个异常处理流程包含以下关键步骤:
硬件检测阶段:
CPU访问VP3时,地址翻译硬件读取对应的页表项(PTE3)
通过PTE3的有效位发现该虚拟页未驻留内存
内核处理阶段:
缺页异常处理程序被调用
选择牺牲页(如PP3中的VP4)进行替换
若VP4被修改过(脏页),则先写回磁盘
更新VP4的页表项,标记为未缓存
数据加载阶段:
从磁盘读取VP3内容到PP3物理页
更新PTE3的有效位和物理页号
建立VP3到PP3的新映射
恢复执行阶段:
异常处理完成后,重新执行触发缺页的指令
此时VP3已缓存在物理内存中
地址翻译硬件能正常完成地址转换
这个过程通过按需调页机制,实现了虚拟内存空间的高效管理,既保证了程序能使用超过物理内存容量的地址空间,又通过局部性原理维持了较好的性能表现。
7.9动态存储分配管理
动态内存管理机制是现代编程语言的核心功能之一,其通过动态内存分配器实现对堆内存的高效管理。在Linux系统中,每个进程的堆内存空间起始于未初始化数据段(.bss)的末尾,由内核维护的brk指针标识当前堆顶位置。当程序调用如printf等函数时,可能会触发malloc调用,此时动态内存分配器将执行以下操作:
内存组织策略:
将堆空间划分为连续的已分配块和空闲块
通过隐式或显式链表维护空闲块信息
采用首次适配、最佳适配或最差适配等算法查找合适空闲块
分配器类型:
显式分配器(如C的malloc/free)要求程序员手动释放内存
隐式分配器(垃圾回收器)自动检测并回收不可达内存块
C标准库采用显式分配策略,通过malloc分配、free释放
分配过程实现:
malloc函数根据请求大小搜索空闲块链表
找到合适块后可能进行分割(避免内部碎片)
返回对齐后的可用内存地址指针
分配失败时可能通过brk/sbrk系统调用扩展堆空间
释放与合并机制:
free函数将释放的块重新加入空闲链表
执行相邻空闲块的合并操作(减少外部碎片)
维护空闲块的最小大小限制
这种动态内存管理机制既提供了灵活的内存使用方式,又通过精巧的设计减少了内存碎片问题,是支撑现代应用程序运行的重要基础。在hello程序中,printf等函数通过调用malloc获取临时内存,使用完毕后应及时释放以避免内存泄漏。
7.10本章小结
本章系统剖析了hello程序运行过程中的内存管理体系,通过五层递进式分析揭示了现代计算机系统的内存管理机制:
地址空间体系架构
深入解析了hello程序运行中涉及的四种关键地址:
逻辑地址:程序生成的段偏移地址,由段选择符和偏移量构成
线性地址:经段式转换后的连续地址空间
虚拟地址:Linux中等同于线性地址的程序视角地址
物理地址:实际DRAM存储单元地址
通过hello反汇编实例,演示了地址转换的全过程及相互关系。
地址转换机制
(1)段式管理实现逻辑到线性地址转换:
段寄存器存储段选择符
GDTR定位全局描述符表
段基址+偏移量生成线性地址
(2)页式管理完成线性到物理地址映射:
四级页表层次结构(PGD→PUD→PMD→PTE)
CR3寄存器定位顶级页表
TLB加速转换过程
结合hello实例分析多级页表空间优化特性
物理内存访问
详述三级缓存(L1-L3)的协同工作机制:
物理地址分解为标记/索引/偏移
8路组相联查询流程
LRU替换策略
包含性缓存层次维护原则
进程内存管理
深入分析hello进程的特殊内存处理:
fork()写时复制机制:
虚拟内存结构复制
页表项只读标记
延迟物理页复制
execve内存重构:
清除原内存映射
新建私有/共享区域
按需调页机制
异常处理与动态分配
(1)缺页异常处理流程:
无效PTE触发异常
牺牲页选择策略
磁盘交换区操作
重执行机制
(2)动态内存管理:
堆空间brk扩展机制
显式分配器实现原理
碎片控制策略
结合printf分析malloc/free应用场景
通过hello程序的完整案例分析,本章构建了从地址转换到物理访问、从进程管理到异常处理的全方位内存管理知识体系,揭示了现代操作系统内存管理的设计哲学与实现细节。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
(以下格式自行编排,编辑时删除)
(第8章 选做 0分)
结论
hello程序的生命周期展现了现代计算机系统完整的程序处理流程,其演进过程可分为六个关键阶段:
源码预处理阶段
预处理处理器(cpp)对hello.c进行宏展开和头文件包含,生成扩展后的hello.i文件。该阶段完成:
删除所有注释内容
解析#include指令并插入头文件内容
展开宏定义和条件编译
保留核心代码逻辑不变
编译转换阶段
编译器(cc1)将预处理后的hello.i转换为汇编文件hello.s,实现:
高级语言到汇编指令的语义转换
基于优化级别(O1/O2等)的代码优化
寄存器分配和指令调度
生成平台相关的AT&T或Intel格式汇编
目标文件生成阶段
汇编器(as)将hello.s转换为可重定位目标文件hello.o,其特征包括:
ELF格式的二进制组织
分节存储代码/数据/符号等信息
未解析的外部引用
可查看的节头表和符号表
链接整合阶段
链接器(ld)合并hello.o与库文件生成可执行文件hello,完成:
符号解析和重定位
合并.text和.data等节区
动态链接库的延迟绑定
虚拟地址空间分配
生成程序头表和入口点信息
进程执行阶段
shell通过fork-exec机制启动hello进程:
fork创建写时复制的地址空间
execve加载程序段并初始化堆栈
动态链接器完成运行时重定位
按需调页机制加载物理页帧
CPU时间片轮转执行指令流
系统回收阶段
进程终止时的资源清理:
关闭所有打开的文件描述符
释放虚拟内存区域
清除进程控制块
向父进程发送终止信号
内核移除进程所有痕迹
整个生命周期贯穿了从源代码到进程执行的完整转换链条,每个阶段都体现了编译系统与操作系统协同工作的精密设计。通过地址转换、动态链接、写时复制等机制,系统实现了高效的资源管理和进程隔离。
附件
文件名
作用
hello.c
原C语言文件
hello.i
预处理产生文件
hello.s
编译产生文件
hello.o
汇编产生文件
hello.out
链接产生可执行文件
hello
链接产生可执行文件
hello_o_elf.txt
查看hello.o的elf格式对应的文本文件
hello_o_asm.txt
查看hello.o的反汇编对应的文本文件
hello_elf.txt
查看hello的elf格式对应的文本文件
hello_asm.txt
查看hello的反汇编对应的文本文件