本报告以经典C程序“hello.c”为载体,系统地分析了从源代码到进程执行的全过程。通过预处理、编译、汇编、链接等步骤,展示了程序如何从文本文件转换为可执行目标文件;结合Linux环境下的进程管理、存储管理和I/O管理机制,详细阐述了Hello程序如何被操作系统加载、调度、执行并最终终止。报告使用GCC、readelf、objdump、gdb等工具对中间结果进行分析,并结合计算机系统层次结构,揭示了程序在硬件与操作系统协同下的完整生命周期。
关键词:预处理;编译;链接;进程管理;虚拟内存;ELF格式;系统调用
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
“P2P”(From Program to Process)过程:
Program:程序员编写hello.c源代码,经过预处理、编译、汇编、链接生成可执行目标文件hello。
Process:在Shell中执行./hello时,操作系统通过fork()创建子进程,再通过execve()加载hello到进程地址空间,从而形成一个活跃的进程。
“O2O”(From Zero-0 to Zero-0):
从无到有:从源代码到进程执行。
从有到无:进程执行结束后被操作系统回收,所有资源释放。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:x86-64架构CPU,8GB RAM
操作系统:Ubuntu 20.04 LTS
开发与调试工具:
GCC 9.3.0(编译链)
readelf、objdump(ELF文件分析)
gdb(调试与执行跟踪)
edb(动态调试)
Git(版本管理)
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
| hello.i | 预处理后的文本文件 |
| hello.s | 汇编语言文件 |
| hello.o | 可重定位目标文件 |
| hello | 可执行目标文件 |
1.4 本章小结
本章介绍了Hello程序的P2P与O2O生命周期,列出了实验环境与中间文件,为后续章节的深入分析奠定基础。
第2章 预处理
2.1 预处理的概念与作用
(以下格式自行编排,编辑时删除)
预处理是编译的第一步,主要处理源代码中以#开头的指令,如:
(1)展开#include包含的头文件内容。
(2)处理#define宏定义。
(3)条件编译(#ifdef、#if等)。
(4)输出为一个纯C代码文本文件(.i),不含任何预处理指令。
2.2在Ubuntu下预处理的命令

图2.1 Ubuntu下预处理命令与结果
结果:生成hello.i文件
2.3 Hello的预处理结果解析
打开hello.i可见:
头文件stdio.h等被完整展开(约上千行)。
原始的hello.c代码位于文件末尾。
所有注释被删除,宏被替换。

图2.2 hello.i内容
2.4 本章小结
预处理将多个源文件与头文件合并为一个完整的文本文件,为编译阶段做好准备。
第3章 编译
3.1 编译的概念与作用
预处理将多个源文件与头文件合并为一个完整的文本文件,为编译阶段做好准备。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令

图3.1 Ubutu下编译的命令
结果:生成hello.s文件
3.3 Hello的编译结果解析
3.3.1 数据:常量、变量、类型
(1)字符串常量

图3.2 对字符串的处理
两个字符串常量存储在.rodata(只读数据段);
.LC0:错误提示字符串,包含中文字符(UTF-8编码显示为八进制转义序列);
.LC1:正常输出格式字符串。
(2)局部变量

图3.3 对局部变量的处理
argc:存储在-20(%rbp),4字节整型;
argv:存储在-32(%rbp),8字节指针;
i:存储在-4(%rbp),4字节整型。
3.3.2 赋值操作
赋初值
![]()
图3.4 对赋初值的处理
使用movl指令进行32位整型赋值;
立即数$0赋值给局部变量i。
3.3.3 类型转换
隐式类型转换(指针到整数)

图3.5 对隐式类型转换的处理
argv[4](char*)作为参数传递给atoi;
atoi返回int存储在%eax;
int值直接传递给sleep函数。
3.3.4 sizeof操作
代码中没有显式sizeof,但编译器隐式使用:
栈分配:subq $32, %rsp,为所有局部变量分配32字节;
指针运算:addq $8, %rax,8字节对应64位系统指针大小。
3.3.5 算术操作
(1)加法

图3.6 对加法的处理
(2)自增
![]()
图3.7 对自增的处理
3.3.6 逻辑操作
逻辑非
![]()
图3.8 对逻辑非的处理
3.3.7 关系操作
相等判断
![]()
图3.9 对相等判断的操作
小于等于判断
![]()
图3.10 对小于等于判断的操作
3.3.8 数组/指针操作
(1)数组访问

图3.11 对数组访问的操作
基地址:argv存储在-32(%rbp);
偏移计算:addq $8, %rax,每次偏移8字节(64位指针);
解引用:movq (%rax), %rax获取数组元素。
(2)地址操作
![]()
![]()
图3.12 对地址的操作
3.3.9 控制转移
(1)if/else结构

图3.13 对if/else分支的操作
(2)for循环操作

图3.14 对for循环的操作
3.3.10 对函数操作
(1)函数调用与参数传递

图3.15函数调用
(2)函数返回

图3.16 对main函数返回的操作
返回过程:
设置返回值:movl $0, %eax,将0放入返回寄存器;
清理栈帧;
返回:ret指令从栈中弹出返回地址并跳转。
3.4 本章小结
编译器将C代码转换为汇编代码,处理了控制流、函数调用、数据访问等,为汇编阶段生成机器指令做准备。
第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将汇编代码.s转换为机器指令(二进制),并生成可重定位目标文件.o
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令

图4.1 Ubuntu下汇编的命令及结果
4.3 可重定位目标elf格式

图4.2 hello.o的ELF格式
4.3.1 ELF头基本信息
hello.o为ELF64格式的可重定位文件,采用小端序,针对x86-64架构。文件入口点为0(不可执行),包含14个节,无程序头表。
主要节分析
(1)代码与数据节
.text节:157字节,包含main函数机器码,地址0x0(待重定位),标志AX(可分配、可执行)
.rodata节:64字节,存储两个字符串常量,8字节对齐,标志A(只读、可分配)
.data/.bss节:大小均为0,无全局变量
(2)关键元数据节
.rela.text:8个重定位条目,用于修正.text中的外部引用
.symtab:18个符号条目,包含main函数和6个未定义库函数符号
.strtab:存储符号名称字符串
.eh_frame:56字节,异常处理信息
4.3.2重定位项目分析
.rela.text节包含8个重定位条目:
(1)本地引用(2个):
.rodata - 4:偏移0x1c,类型R_X86_64_PC32,指向错误提示字符串
.rodata + 0x2c:偏移0x5f,指向"Hello %s %s %s\n"字符串
(2)库函数引用(6个):
puts - 4:偏移0x21,对应call puts@PLT
exit - 4:偏移0x2b,对应call exit@PLT
printf - 4:偏移0x69,对应call printf@PLT
atoi - 4:偏移0x7c,对应call atoi@PLT
sleep - 4:偏移0x83,对应call sleep@PLT
getchar - 4:偏移0x92,对应call getchar@PLT
重定位类型说明:
R_X86_64_PC32:PC相对寻址,用于本地数据访问
R_X86_64_PLT32:PLT跳转,用于动态库函数调用
4.3.3符号表关键条目
(1)已定义符号:
main:FUNC类型,157字节,在.text节中,全局可见
(2)未定义符号(UND):
puts, exit, printf, atoi, sleep, getchar:C库函数
_GLOBAL_OFFSET_TABLE_:全局偏移表引用
4.3.4文件布局特点
(1)地址未绑定:所有节起始地址均为0,需链接时确定
(2)外部依赖:包含6个库函数的外部引用
(3)重定位信息完整:提供代码中所有地址引用的修正信息
(4)无执行信息:缺少程序头表和入口地址,不能直接运行
4.3.5与程序关联
.text节的157字节对应main函数汇编代码;
.rodata节的64字节存储程序中的中英文字符串;
8个重定位条目对应程序中6个函数调用和2个字符串引用;
符号表中的main为唯一已定义的全局符号。
4.4 Hello.o的结果解析

图4.3 hello.o反汇编的结果
4.4.1 反汇编与汇编代码对照分析
(1)函数开头部分
汇编指令与机器码一一对应,例如push %rbp对应机器码55;
立即数表示:汇编中的十进制数在机器码中转换为十六进制,如$32变为$0x20;
偏移量表示:-20(%rbp)在机器码中为-0x14(%rbp),两者等价(20的十六进制为0x14)。
(2)条件判断与跳转
条件跳转指令je的机器码为74,后跟8位偏移量0x16;
在汇编代码中使用标签.L2,在机器码中转换为绝对地址0x2f;
偏移量计算:目标地址0x2f - 下条指令地址0x19 = 0x16。
(3)字符串地址加载与函数调用
lea指令的机器码中包含4个字节的占位符00 00 00 00,等待重定位;
重定位条目R_X86_64_PC32 .rodata-0x4指示如何修正该地址;
call指令同样使用占位符,重定位条目为R_X86_64_PLT32 puts-0x4。
(4)循环结构
同一节内的跳转已由汇编器计算完成;
jmp .L3转换为jmp 8b,偏移量0x53 = 0x8b - 0x38;
jle .L4转换为jle 38,偏移量0xa7(有符号数-89)= 0x38 - 0x91。
(5)printf函数参数准备
汇编指令与机器码完全对应,只是地址表示方式不同;
最后的lea指令包含占位符,重定位条目指向.rodata+0x2c(第二个字符串)。
(6)其他函数调用
所有外部函数调用都使用占位符00 00 00 00;
每个call指令都有对应的重定位条目,类型为R_X86_64_PLT32。
4.4.2 机器语言的构成与映射关系
(1)机器语言的基本构成
机器语言由操作码(opcode)和操作数(operand)组成:
操作码:指示执行什么操作(如55表示push %rbp);
操作数:提供操作所需的数据或地址。
(2)与汇编语言的主要差异
a.地址表示方式不同:
汇编语言:使用符号标签(如.L2、.LC0);
机器语言:使用绝对或相对地址(如0x2f、0x0(%rip))。
b.外部引用处理不同:
汇编语言:使用完整符号(如puts@PLT);
机器语言:使用占位符(00 00 00 00)+ 重定位信息。
c.立即数表示不同:
汇编语言:十进制或符号(如$32、$5);
机器语言:十六进制(如$0x20、$0x5)。
(3)分支转移的地址处理
a.条件跳转
汇编语言:je .L2
机器语言:74 16
汇编器计算.L2与下条指令的距离,生成8位有符号偏移量,运行时PC加上偏移量实现跳转。
b.函数调用
汇编语言:call printf@PLT
机器语言:e8 00 00 00 00
汇编时无法确定printf的实际地址,生成32位占位符,等待链接器修正,通过重定位条目R_X86_64_PLT32指导链接器修正。
(4)寻址模式编码
a.立即数寻址:movl $0, -4(%rbp) # c7 45 fc 00 00 00 00
b.寄存器间接寻址:mov (%rax), %rcx # 48 8b 08
c.RIP相对寻址:lea .LC1(%rip), %rdi # 48 8d 3d 00 00 00 00
4.4.3 小结
(1)汇编过程的核心任务是生成可重定位的机器代码,其中同一模块内的引用已解析,外部引用留待链接时处理。
(2)机器语言与汇编语言的根本区别在于地址表示方式:汇编语言使用人类可读的符号,机器语言使用数值地址或占位符。
(3)重定位机制是连接编译时和链接时的关键桥梁,确保代码可以在不同地址加载执行。
(4)理解这种映射关系对于调试、优化和系统级编程至关重要,它揭示了高级语言到底层硬件的转换过程。
4.5 本章小结
汇编阶段生成包含机器码的可重定位目标文件,但外部符号地址尚未解析,需链接阶段完成。
第5章 链接
5.1 链接的概念与作用
链接器(ld)将多个.o文件及库文件合并为一个可执行文件,完成:
符号解析:将符号引用与定义关联;
重定位:将符号地址修正为最终内存地址。
注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令

图5.1 Ubuntu下链接的命令
5.3 可执行目标文件hello的格式

图5.2 hello的ELF格式
5.4 hello的虚拟地址空间

图5.3 hello的虚拟地址空间
| 虚拟地址范围 | 文件偏移 | 大小 | ELF对应段 | 实际内容 | 权限 |
| 0x400000-0x401000 | 0x0 | 4KB | LOAD段1 | ELF头、.interp、.note等 | 只读 |
| 0x401000-0x402000 | 0x1000 | 4KB | LOAD段2 | .init、.plt、.text、.fini | 可执行 |
| 0x402000-0x403000 | 0x2000 | 4KB | LOAD段3 | .rodata、.eh_frame | 只读 |
| 0x403000-0x404000 | 0x2000 | 4KB | LOAD段4(部分) | .data、.dynamic、.got等 | 读写 |
| 0x404000-0x405000 | 0x3000 | 4KB | (无直接对应) | .bss或其他运行时数据 | 读写 |
表5.1 与ELF对照分析
5.5 链接的重定位过程分析
5.5.1 hello与hello.o的主要差异
通过对比hello.o(可重定位目标文件)和hello(可执行文件)的反汇编结果,可以观察到以下关键差异:
(1)地址解析完成
hello.o:所有外部引用和跨节引用使用占位符(全0)
hello:所有地址已被解析为具体值,包括函数调用和字符串引用
(2) 新增启动和终止代码
hello.o:仅包含用户定义的main函数
hello:添加了_start、_init、_fini、__libc_csu_init、__libc_csu_fini等代码,这些来自C运行时库
(3)PLT(过程链接表)和GOT(全局偏移表)机制
hello.o:使用@PLT标记,但实际调用地址未确定
hello:PLT条目已建立,包含完整的动态链接跳转表
(4)节合并与地址分配
hello.o:所有节起始地址为0
hello:各节被分配到具体的虚拟地址(如.text段从0x401000开始)
5.5.2 链接过程的核心机制
链接过程主要包括两个核心任务:符号解析和重定位。
(1)符号解析
链接器将每个符号引用与一个符号定义关联起来。对于hello程序:
puts、printf、getchar、atoi、exit、sleep:解析到libc共享库中的定义;
main:用户定义的函数,作为程序的入口;
.rodata中的字符串:解析到只读数据段的特定位置。
(2)重定位
链接器根据重定位条目修改目标代码,将符号地址填入需要重定位的位置。
5.5.3 具体重定位项目分析
基于之前提供的hello.o重定位条目,分析链接器如何在hello中完成重定位:
(1)第一个重定位条目:字符串引用
hello.o重定位:
偏移量: 0x1c, 类型: R_X86_64_PC32, 符号: .rodata, 加数: -4
hello中对应的指令:
40113e: 48 8d 3d c3 0e 00 00 lea 0xec3(%rip),%rdi
重定位计算过程:
公式:S + A - P(对于R_X86_64_PC32类型)
S:符号地址(.rodata中字符串地址)= 0x402008
A:加数 = -4
P:重定位位置地址 = 0x40113e + 3 = 0x401141(偏移字段在指令中的地址)
计算:0x402008 - 4 - 0x401141 = 0xec3
验证:指令中的偏移值正是0xec3,正确!
作用:将lea指令的RIP相对寻址指向正确的字符串地址(错误提示信息)。
(2)第二个重定位条目:puts函数调用
hello.o重定位:
偏移量: 0x21, 类型: R_X86_64_PLT32, 符号: puts, 加数: -4
hello中对应的指令:
401145: e8 46 ff ff ff callq 401090 <puts@plt>
重定位计算过程:
公式:L + A - P(对于R_X86_64_PLT32类型)
L:PLT条目地址 = 0x401090(puts@plt)
A:加数 = -4
P:重定位位置地址 = 0x401145 + 1 = 0x401146
计算:0x401090 - 4 - 0x401146 = -0xba = 0xffffff46(32位有符号数)
验证:指令中的偏移值0xffffff46(小端表示为46 ff ff ff)正是-0xba
作用:将call指令重定向到PLT条目,实现动态链接的延迟绑定。
(3)第三个重定位条目:exit函数调用
hello.o重定位:
偏移量: 0x2b, 类型: R_X86_64_PLT32, 符号: exit, 加数: -4
hello中对应的指令:
40114f: e8 7c ff ff ff callq 4010d0 <exit@plt>
计算:类似puts,重定位到exit@plt地址0x4010d0。
(4)第四个重定位条目:printf格式字符串
hello.o重定位:
偏移量: 0x5f, 类型: R_X86_64_PC32, 符号: .rodata+0x2c, 加数: +0x2c
hello中对应的指令:
401181: 48 8d 3d b0 0e 00 00 lea 0xeb0(%rip),%rdi
计算:
S = 字符串"Hello %s %s %s\n"地址 = 0x402038
P = 0x401181 + 3 = 0x401184
偏移值 = 0x402038 - 0x401184 = 0xeb4
(5) 第五至八个重定位条目:其他函数调用
printf、atoi、sleep、getchar:都类似地重定位到相应的PLT条目
5.6 hello的执行流程
| 阶段 | 函数/地址 | 作用 | 调用者 | 被调用者 |
| 启动 | 0x4010f0 (_start) | 程序入口点 | 操作系统 | __libc_start_main |
| 初始化 | 0x401000 (_init) | 全局初始化 | __libc_csu_init | gmon_start |
| C库构造 | 0x4011d0 (__libc_csu_init) | C++全局对象构造 | __libc_start_main | _init |
| 主程序 | 0x401125 (main) | 用户主函数 | __libc_start_main | puts/printf/atoi/sleep/getchar |
| 错误处理 | 0x401090 (puts@plt) | 打印错误信息 | main | libc puts函数 |
| 正常输出 | 0x4010a0 (printf@plt) | 格式化输出 | main | libc printf函数 |
| 输入处理 | 0x4010b0 (getchar@plt) | 读取字符 | main | libc getchar函数 |
| 类型转换 | 0x4010c0 (atoi@plt) | 字符串转整数 | main | libc atoi函数 |
| 程序退出 | 0x4010d0 (exit@plt) | 退出程序 | main | libc exit函数 |
| 延时操作 | 0x4010e0 (sleep@plt) | 休眠指定秒数 | main | libc sleep函数 |
| C库析构 | 0x401240 (__libc_csu_fini) | C++全局对象析构 | exit | - |
| 终止 | 0x401248 (_fini) | 全局终止函数 | exit | - |
表5.2 调用与跳转的各个子程序名或程序地址
5.7 Hello的动态链接分析

图5.4 动态链接前后项目变化
5.8 本章小结
链接将多个模块合并为可执行文件,完成地址重定位,并支持动态链接库的延迟绑定。
第6章 hello进程管理
6.1 进程的概念与作用
进程是程序执行的实例,拥有独立的地址空间、文件描述符、寄存器上下文等。OS通过进程实现多任务并发。
6.2 简述壳Shell-bash的作用与处理流程
作用:
(1)读取用户输入的命令行。
(2)解析参数,判断是否为内置命令。
处理流程:
对于外部命令(如./hello),调用fork()创建子进程,再execve()加载程序。
6.3 Hello的fork进程创建过程
fork()复制当前进程(Shell)创建一个几乎完全相同的子进程,包括代码、数据、堆栈。子进程返回0,父进程返回子进程PID。
6.4 Hello的execve过程
execve("./hello", argv, envp):
加载hello的ELF文件到内存;
替换当前进程的代码段、数据段;
跳转到hello的入口地址开始执行。
6.5 Hello的进程执行
6.5.1进程创建与初始化
当用户在Shell中输入./hello 12345678 张三 13800138000 2时,Shell进程首先调用fork()系统调用创建子进程。fork()复制当前Shell的进程控制块(PCB)、地址空间和打开文件表,生成一个几乎完全相同的子进程。随后子进程调用execve()系统调用,该调用会销毁子进程原有的地址空间,根据hello程序的ELF格式重新建立内存映射:代码段映射到0x401000-0x402000(可读可执行),只读数据段映射到0x402000-0x403000,读写数据段映射到0x403000-0x404000。动态链接器/lib64/ld-linux-x86-64.so.2负责加载共享库libc,并重定位GOT表中的函数地址。完成这些初始化后,进程从入口点_start(0x4010f0)开始执行,经过C运行时初始化后调用main函数。
6.5.2 进程调度与时间片管理
Hello进程被创建后,操作系统将其加入运行队列。Linux使用完全公平调度器(CFS)管理进程执行,CFS维护一个按虚拟运行时间(vruntime)排序的红黑树。Hello作为普通进程,其权重值为1024(nice值为0的默认权重)。在典型的桌面配置中,调度周期为6毫秒,当系统中只有Shell和Hello两个权重相同的进程时,每个进程获得3毫秒的时间片。Hello进程的执行被划分为一系列时间片:前3毫秒执行初始化代码和第一次printf输出,然后时钟中断触发,内核将Hello的vruntime增加3毫秒,发现时间片用完,于是设置重新调度标志。中断返回前,调度器选择Shell进程执行。3毫秒后,Shell时间片用完,Hello再次被调度执行。这种时间片轮转在整个Hello生命周期中持续发生,大约每3毫秒切换一次,在Hello的20秒总运行时间内会发生约6666次调度切换。
6.5.3 上下文切换机制
每次调度发生时,操作系统需要执行完整的上下文切换。当Hello的时间片用完时,时钟中断触发,CPU从用户态陷入内核态。内核首先保存Hello的完整寄存器上下文:包括通用寄存器%rax、%rbx、%rcx、%rdx,栈指针%rsp,指令指针%rip,以及浮点寄存器、向量寄存器等。这些寄存器值被保存在Hello进程的内核栈中,并通过PCB的thread字段持久化。接着内核调用pick_next_task()从运行队列中选择下一个要运行的进程(通常是Shell)。如果下一个进程使用不同的地址空间,还需要切换页表基址寄存器%cr3,这会导致TLB刷新和缓存失效。然后内核从Shell的PCB中恢复其保存的寄存器值,将栈指针切换到Shell的内核栈,最后通过iretq指令返回用户态继续执行Shell。整个上下文切换过程大约消耗1000-2000个CPU周期,加上缓存失效的开销,总时间约为5-10微秒。在Hello的20秒运行期间,上下文切换的总开销约为33毫秒,占执行时间的0.17%。
6.5.4 用户态与核心态转换
Hello进程在执行过程中频繁在用户态和核心态之间转换。这种转换主要通过三种机制触发:系统调用、中断和异常。当Hello调用printf()时,实际通过PLT跳转到libc的write实现,最终执行syscall指令触发系统调用门。CPU自动切换到内核栈,保存用户态寄存器,根据系统调用号查找系统调用表,执行sys_write内核函数处理输出请求。完成服务后,内核恢复用户态寄存器返回到Hello进程。类似地,sleep(2)调用会触发sys_nanosleep系统调用,内核将Hello进程状态标记为TASK_INTERRUPTIBLE,设置2秒定时器后调用schedule()主动放弃CPU。除了显式系统调用外,时钟中断每3毫秒触发一次,强制Hello陷入内核更新时间统计,检查时间片是否用完。硬件中断如键盘输入也会导致态转换。此外,当Hello访问未映射的虚拟页面时,会发生缺页异常,陷入内核分配物理页面并建立映射。在整个执行过程中,Hello大约进行40次显式态转换(20次系统调用,每次进出内核各一次)和约6666次中断触发的态转换。
6.6 hello的异常与信号处理
| 异常类型 | 触发条件 | 产生信号 | 处理方式 |
| 硬件中断 | 定时器中断、键盘中断 | 无直接信号 | 内核处理,可能引起调度 |
| 系统调用异常 | 无效系统调用参数 | 无 | 返回错误码 |
| 缺页异常 | 访问未映射内存 | SIGSEGV | 内核分配物理页 |
| 算术异常 | 除零错误 | SIGFPE | 终止进程 |
| 非法指令 | 执行无效机器码 | SIGILL | 终止进程 |
表6.1 异常与信号处理
| 信号 | 触发方式 | 默认行为 | 说明 |
| SIGINT (2) | Ctrl-C | 终止进程 | 中断进程 |
| SIGTSTP (20) | Ctrl-Z | 暂停进程 | 挂起进程 |
| SIGCONT (18) | fg命令 | 继续执行 | 恢复暂停的进程 |
| SIGQUIT (3) | Ctrl-\ | 终止+core | 强制终止 |
| SIGKILL (9) | kill -9 | 强制终止 | 不可捕获 |
| SIGSEGV (11) | 非法内存访问 | 终止+core | 段错误 |
表6.2 信号类型

图6.1 各命令及运行结果
6.7本章小结
进程管理使得Hello能在OS调度下执行,并支持用户交互与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序代码中的地址(如%rip值)。
线性地址(虚拟地址):经过段式管理转换后的地址。
物理地址:实际内存芯片上的地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
x86-64使用平坦模式,逻辑地址直接映射为线性地址(段基址为0)。
7.3 Hello的线性地址到物理地址的变换-页式管理
通过页表实现虚拟页到物理页的映射(通常页大小为4KB)。
7.4 TLB与四级页表支持下的VA到PA的变换
四级页表:CR3指向顶级页目录,经过4级索引找到物理页。
TLB缓存最近使用的页表项,加速转换。
7.5 三级Cache支持下的物理内存访问
CPU访问数据时,依次查找L1、L2、L3 Cache,未命中则访问主存。
7.6 hello进程fork时的内存映射
fork()使用写时复制(Copy-On-Write):父子进程共享物理页,直到一方尝试写入时才复制。
7.7 hello进程execve时的内存映射
execve()建立新的地址空间:
代码段、数据段从ELF文件映射;
堆、栈初始化为空。
7.8 缺页故障与缺页中断处理
访问未加载的页时触发缺页异常,OS从磁盘加载页面到内存,并更新页表。
7.9动态存储分配管理
printf内部可能调用malloc分配缓冲区,使用隐式空闲链表、分离空闲链表等算法管理堆内存。
7.10本章小结
存储管理通过虚拟内存、页表、Cache等机制,为Hello提供了透明、高效、安全的地址空间。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux将设备抽象为文件,通过VFS(虚拟文件系统)统一管理。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
open、read、write、close:低级文件操作。
printf、scanf:标准库封装。
8.3 printf的实现分析
(1)vsprintf格式化字符串到缓冲区。
(2)write系统调用将缓冲区写入标准输出文件描述符(1)。
(3)内核通过字符设备驱动将字符写入显示器VRAM。
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(1)键盘中断将扫描码转换为ASCII码存入缓冲区。
(2)read系统调用从标准输入(0)读取字符。
(3)getchar等待回车后返回第一个字符。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
Linux的IO管理通过文件抽象和系统调用,使得Hello能方便地与键盘、显示器等设备交互
结论
1.Hello从源代码到可执行文件经历了预处理、编译、汇编、链接四个阶段。
2.在Shell中执行时,OS通过fork和execve创建进程,并为其分配虚拟地址空间。
3.进程执行过程中,CPU通过页表、TLB、Cache实现地址转换与数据访问。
4.Hello通过系统调用与标准库函数完成IO操作,最终由OS回收资源。
感悟:一个简单的Hello程序背后,是编译系统、操作系统、硬件体系结构的紧密协作。计算机系统的设计体现了分层抽象与协同工作的哲学,每一层都为上层提供简洁接口,隐藏底层复杂性。
参考文献
[1] Randal E. Bryant, David R. O’Hallaron. Computer Systems: A Programmer's Perspective. 3rd ed.
[2] GCC Manual. https://gcc.gnu.org/onlinedocs/
[3] Linux Manual Pages. https://man7.org/linux/man-pages/
[4] ELF Format Specification. http://refspecs.linuxfoundation.org/elf/elf.pdf
[5] Intel® 64 and IA-32 Architectures Software Developer Manuals.
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/kejixiaobaili/article/details/156576298



