本项目以“程序人生-Hello's P2P”为核心,通过编写、预处理、编译、汇编、链接及运行一个简单的Hello程序,系统探讨了计算机系统中程序从代码到进程的全生命周期。实验基于Ubuntu环境,使用GCC工具链完成代码转换,分析了预处理展开头文件、编译生成汇编指令、汇编生成可重定位目标文件、链接生成可执行文件等关键步骤的底层机制。结合进程管理、存储管理和IO管理,深入揭示了操作系统对程序执行的资源分配、地址转换、异常处理及动态链接的支持。项目通过理论与实践结合,完整呈现了程序在编译系统、操作系统和硬件协同下的运行流程,验证了计算机系统分层抽象与模块化设计的高效性,为深入理解系统级编程与优化提供了重要参考。
关键词:预处理;编译;链接;进程管理;存储管理;动态链接;IO管理
目 录
第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 -
结论......................................................................................................................... - 14 -
附件......................................................................................................................... - 15 -
参考文献................................................................................................................. - 16 -
第1章 概述
1.1 Hello简介
Program:在VSCode中编写hello.c
预处理:gcc -E hello.c -o hello.i → 展开头文件
编译:gcc -S hello.i -o hello.s → 生成x86_64汇编
汇编:gcc -c hello.s -o hello.o → 生成ELF可重定位文件
链接:gcc hello.o -o hello → 动态链接glibc 2.38
Process:Shell通过fork()创建子进程,execve()加载hello。
OS分配虚拟内存,CPU执行指令后调用exit_group(0)终止。
020(From Zero to Zero):0→1:文本文件→可执行文件
1→0:进程终止后,OS回收页表、文件描述符等资源。
1.2 环境与工具
硬件 x86_64 CPU, DELL服务器(Ubuntu)
内核 Linux 6.8.0-54-generic
工具链 GCC 13.3.0(-Og)、GNU Binutils 2.41、glibc 2.38
调试 GDB 14.2、strace 6.8、objdump 2.41
编辑器 VSCode
1.3 中间结果
hello.i # 预处理后代码
hello.s # 汇编代码
hello.o # ELF文件
hello # 动态链接可执行文件
1.4 本章小结
本章简述了Hello程序的P2P(从代码到进程)和020(从无到无)生命周期,列出了实验环境和生成的中间文件。后续章节将深入分析预处理、编译、进程运行等细节。
第2章 预处理
2.1 预处理的概念与作用
预处理(Preprocessing)是C程序编译的第一步,由预处理器(cpp)执行,主要完成以下任务:
宏展开:替换#define定义的宏
头文件包含:递归展开#include指令,插入头文件内容
条件编译:处理#if、#ifdef等指令,决定是否包含代码块
删除注释:移除所有/* ... */和//注释
添加行标记:插入#line指令,便于调试时定位源码位置。
作用:
使代码可移植(如通过条件编译适配不同平台)
减少重复代码(通过头文件和宏)
生成“纯净”的C代码(不含预处理指令),供编译器处理
2.2在Ubuntu下预处理的命令
-m64:64位 -E:仅预处理 -o hello.i:输出到hello.i文件。
2.3 Hello的预处理结果解析
头文件展开:
原始代码中的#include被替换为头文件内容:
stdio.h:插入printf、exit等函数声明
unistd.h:插入sleep、getchar等系统调用声明
stdlib.h:插入atoi的函数声明
注释删除:
源码开头的注释被完全移除
行标记:
插入#line指令,标识原始代码位置
2.4 本章小结
本章通过gcc -E命令对hello.c进行预处理,生成hello.i文件,并分析其内容,进行预处理核心任务:头文件展开(插入stdio.h等)、删除注释、添加行标记。原始代码从24行扩展为3180行(主要因头文件插入),保留了所有函数调用(printf、sleep)的声明。
第3章 编译
3.1 编译的概念与作用
概念:
编译(Compilation)指将预处理后的C代码(.i)转换为汇编代码(.s)的过程,由编译器(GCC的cc1组件)完成
作用:
语法分析:检查代码是否符合C语言规范
语义优化:在-Og优化级别下,平衡可读性与性能
生成汇编:将高级C代码转换为低级机器指令的助记符
3.2 在Ubuntu下编译的命令
-m64:64位 -S:生成汇编代码 -Og:启用调试优化
3.3 Hello的编译结果解析
3.3.1 字符串处理
.LC0:
.string "\347\224\250\346\263\225..." # UTF-8编码的中文字符串
.LC1:
.string "Hello %s %s %s\n" # printf格式字符串
3.3.2 函数框架
函数入口:
main:
endbr64 # 对抗ROP攻击的指令
pushq %rbp # 保存基址指针
pushq %rbx # 保存被调用者保存寄存器
subq $8, %rsp # 分配栈空间
3.3.3 参数检查
cmpl $5, %edi # 比较argc和5
jne .L6 # 不等于则跳转错误处理
movq %rsi, %rbx # 保存argv指针
3.3.4 错误处理
.L6:
leaq .LC0(%rip), %rdi # 加载错误字符串地址
call puts@PLT # 调用puts输出
movl $1, %edi
call exit@PLT # 退出程序
3.3.5 主循环结构
循环初始化:movl $0, %ebp # i = 0
循环条件:
.L2:
cmpl $9, %ebp # 比较i和9
jle .L3 # i <= 9时继续循环
循环增量:addl $1, %ebp # i++
3.3.6 函数调用
printf调用:
movq 24(%rbx), %rcx # argv[3]
movq 16(%rbx), %rdx # argv[2]
movq 8(%rbx), %rsi # argv[1]
leaq .LC1(%rip), %rdi # 格式字符串
movl $0, %eax # 清空浮点寄存器
call printf@PLT
其他调用:
call atoi@PLT # 字符串转整数
call sleep@PLT # 延时
call getchar@PLT # 等待输入
3.3.7 函数返回
movl $0, %eax # 返回值0
addq $8, %rsp # 释放栈空间
popq %rbx # 恢复寄存器
popq %rbp
ret
3.4 本章小结
编译器将C代码准确转换为x86_64汇编指令,优化选项-Og保持了代码可读性,完整实现了:参数检查、循环控制、多参数函数调用、系统调用。符合x86_64调用约定。
使用RIP相对寻址访问字符串常量,参数通过寄存器传递(rdi, rsi, rdx, rcx),循环转换为条件跳转结构,完整保留了程序的控制流逻辑。
第4章 汇编
4.1 汇编的概念与作用
概念:
汇编是将汇编代码(.s)转换为机器语言二进制目标文件(.o)的过程,由汇编器完成
作用:
指令编码:将汇编助记符转换为机器码
生成可重定位目标文件:包含代码、数据、符号表和重定位信息,供链接器使用
4.2 在Ubuntu下汇编的命令
-c:仅汇编不链接 -m64:生成64位目标文件
4.3 可重定位目标elf格式
4.3.1 节头基本信息
通过 readelf -S hello.o 分析目标文件结构:
节名 | 类型 | 大小 | 标志 | 说明 |
.text | PROGBITS | 0x75 | AX | 主函数机器指令(可执行代码) |
.rela.text | RELA | 0xc0 | I | .text节的重定位条目(需链接器修正地址) |
.rodata.str1.8 | PROGBITS | 0x30 | AMS | 8字节对齐的只读字符串(中文错误提示.LC0) |
.rodata.str1.1 | PROGBITS | 0x10 | AMS | 1字节对齐的只读字符串(Hello格式串.LC1) |
.eh_frame | PROGBITS | 0x40 | A | 异常处理帧信息(用于栈展开) |
.symtab | SYMTAB | 0x120 | - | 符号表(函数和全局变量名) |
4.3.2 重定位项目分析
通过 readelf -r hello.o 查看重定位条目:
R_X86_64_PC32(本地符号相对地址修正)
作用:加载字符串地址(.LC0)
指令:lea .LC0(%rip), %rdi
公式:最终地址=Symbol地址−Offset−4
R_X86_64_PLT32(外部函数PLT跳转修正)
作用:调用动态库函数(printf、sleep)
指令:call printf@PLT
公式:PLT偏移=PLT表基址+Symbol在PLT中的索引×16−Offset−4
4.4 Hello.o的结果解析
通过 objdump -d -r hello.o 输出的反汇编结果,结合第3章的 hello.s 汇编代码,对 hello.o 的机器语言构成、重定位条目及与汇编的映射关系进行详细分析。
4.4.1汇编与反汇编的对比分析
4.4.1.1 函数入口与栈帧构建
汇编代码(hello.s):
反汇编(hello.o):
分析:
机器码:f3 0f 1e fa:endbr64(Intel CET指令,防御ROP攻击)。
55 53 48 83 ec 08:保存 %rbp、%rbx,分配8字节栈空间。
操作数编码:无重定位需求,偏移量直接编码(sub $0x8,%rsp 对应 48 83 ec 08)。
4.4.1.2 参数检查与错误处理
汇编代码(hello.s):
反汇编(hello.o):
分析:
机器码:83 ff 05:cmpl $5, %edi(比较argc与5)。
75 0a:jne(条件不满足时跳转至偏移量0x19)。
跳转目标:目标地址 0x19 在汇编阶段已计算为绝对偏移,无需重定位。
4.4.1.3 错误处理分支(.L6)
汇编代码(hello.s):
反汇编(hello.o):
分析:
字符串加载(.LC0):
机器码:48 8d 3d 00 00 00 00
lea指令的PC相对寻址,占位符 00 00 00 00 需重定位
重定位条目:R_X86_64_PC32
修正值 = .LC0地址 - 下一条指令地址(0x20) - 4
函数调用(puts@PLT):
机器码:e8 00 00 00 00
call指令的PLT表跳转,占位符需链接器填充
重定位条目:R_X86_64_PLT32
修正值 = puts@PLT入口地址 - 下一条指令地址(0x25) - 4
4.4.1.4 主循环逻辑(.L2-.L3)
汇编代码(hello.s):
反汇编(hello.o):
分析:
(1)参数传递机制
argv访问: %rbx 存储 argv 数组基址,偏移量计算
mov 0x8(%rbx), %rsi # argv[1] = *(argv + 8) 机器码 48 8b 73 08
48:64位操作数前缀 8b 73 08:mov指令编码,源地址为 %rbx + 8
printf参数传递:
寄存器顺序:%rdi(格式串)、%rsi(argv[1])、%rdx(argv[2])、%rcx(argv[3])。
浮点处理:mov $0x0, %eax 表示无向量寄存器参数。
(2)控制流与重定位
循环跳转(jle .L3) 机器码:7e cb
7e:jle操作码 cb:8位偏移量(补码 -53),目标地址为 0x62 - 53 + 2 = 0x2f
无需重定位,偏移量在汇编阶段确定。
外部函数调用(sleep@PLT) 机器码:e8 00 00 00 00
占位符 00 00 00 00 需链接器填充
R_X86_64_PLT32,通过PLT表实现动态链接。
4.4.1.5 程序退出与栈帧回收
汇编代码(hello.s):
反汇编(hello.o):
分析:
栈平衡 addq $8, %rsp 回收栈空间,popq恢复保存的寄存器
返回值 mov $0x0, %eax 设置返回值0
4.4.2 机器语言构成
机器语言由操作码和操作数组成,以二进制形式编码,具有以下特征:
定长操作码 call 对应 e8,movq 对应 48 8b
变长操作数 地址偏移、立即数等长度可变(1-8字节)
指令前缀 48 表示64位操作数,f3 0f 1e fa 表示安全指令 endbr64
4.4.3机器语言与汇编语言的映射关系
汇编指令 | 机器码 | 操作数编码解析 | |
数据操作指令 | movq 8(%rbx), %rsi | 48 8b 73 08 | 48(64位前缀)+ 8b(mov)+ 73 08(%rbx+8 → %rsi) |
lea .LC0(%rip), %rdi | 48 8d 3d 00 00 00 00 | 48 8d(lea)+ 3d(%rdi目标)+ 00 00 00 00(重定位) | |
控制流 | call printf@PLT | e8 00 00 00 00 | e8(call)+ 00 00 00 00(重定位) |
jle .L3 | 7e cb | 7e(jle)+ cb(8位偏移量 -53) |
4.4.4机器语言与汇编语言不一致性分析
分支转移指令 如jle .L3 → 7e cb
汇编语言使用符号标签(.L3)机器语言编码为相对偏移量,与当前指令地址无关。
偏移计算:目标地址 0x2f = 当前地址 0x62 + 偏移 0xcb(-53) + 指令长度 2。
函数调用指令 如call puts@PLT → e8 00 00 00 00
汇编语言显式标注符号(puts@PLT)机器语言使用占位符,依赖重定位条目修正。
重定位修正:根据 R_X86_64_PLT32 类型,链接时填充PLT表偏移。
地址加载指令 如lea .LC0(%rip), %rdi → 48 8d 3d 00 00 00 00
汇编语言使用符号(.LC0)和相对寻址(%rip)
机器语言编码为 PC相对偏移占位符,需重定位修正。
重定位修正:根据 R_X86_64_PC32 类型,计算 .LC0 与 %rip 的相对偏移。
4.5 本章小结
本章分析了可重定位目标文件 hello.o 的生成与结构,揭示了汇编到机器语言的转换机制。目标文件通过 .text 节存储机器指令,.rela.text 记录需重定位的符号地址。机器语言由操作码和操作数构成,分支指令直接编码相对偏移,函数调用通过占位符预留重定位空间。安全设计上,endbr64 指令与ELF属性节增强了控制流完整性。本章为理解链接过程与指令集架构协作提供了底层视角。
第5章 链接
5.1 链接的概念与作用
链接是将多个目标文件和库文件合并为可执行文件的过程,包括:
符号解析:将未定义的符号(如 printf)绑定到动态库中的实际地址
地址重定位:修正代码和数据段的绝对地址,生成可执行内存布局
节合并:整合不同目标文件的 .text、.data 等节,生成最终ELF结构
5.2 在Ubuntu下链接的命令
-dynamic-linker:指定动态链接器路径 crt1.o 提供程序入口 _start
crti.o 和 crtn.o 包含初始化代码 -lc 链接 libc.so,提供标准库函数
5.3 可执行目标文件hello的格式
段名 | 起始地址 | 大小 | 权限 | 说明 |
.interp | 0x4002e0 | 0x1c | R | 存储动态链接器路径 |
.plt | 0x401020 | 0x70 | RX | 过程链接表(PLT),用于跳转到动态库函数 |
.text | 0x4010f0 | 0xaa | RX | 主程序代码,包含 _start、main 和 _dl_relocate_static_pie |
.rodata | 0x402000 | 0x48 | R | 只读数据段,存储字符串常量 |
.got.plt | 0x403fe8 | 0x48 | RW | 全局偏移表(GOT),存储动态库函数的实际地址 |
.data | 0x404030 | 0x4 | RW | 已初始化的全局数据 |
.dynamic | 0x403e38 | 0x1a0 | RW | 动态链接信息,包含依赖库列表和重定位条目 |
5.4 hello的虚拟地址空间
使用edb加载hello,查看虚拟地址空间各段信息,并与5.3对照分析
通过对比edb加载的虚拟地址空间信息与5.3中的ELF段信息,分析如下:
1. 只读代码段(R--/R-X)
内存区域:
对应ELF段:
0x400000-0x401000(R--):包含.interp(0x4002e0)、.note、.hash等只读段,与ELF权限一致
0x401000-0x402000(R-X):包含可执行段.plt(0x401020)、.text(0x4010f0),权限与ELF中标记的RX完全匹配
0x402000-0x403000(R--):对应只读数据段.rodata(0x402000),权限一致
2. 可读写数据段(RW)
内存区域:
对应ELF段:
.dynamic(0x403e38):存储动态链接信息,权限为RW。
.got.plt(0x403fe8):全局偏移表,需运行时修改,权限为RW。
.data(0x404030):已初始化全局数据,权限为RW。
3. 动态链接库与系统区域
内存区域:
[stack]、[vvar]、[vdso]、[vsyscall]为系统保留区域
动态链接库的段不属于hello的ELF文件,但在进程虚拟地址空间中加载
[stack]为运行时栈,[vdso]为内核提供的虚拟动态共享对象,与5.3中的段无关
4. 权限合并与对齐
ELF文件中的多个段(如.interp、.note)被合并到同一内存页(0x400000-0x401000),权限取并集(R--)。
可执行段(.text、.plt)合并到R-X区域,符合代码段的典型权限。
5.5 链接的重定位过程分析
对比 hello.o 和 hello 的反汇编代码,解析重定位过程
1. 函数调用重定位(以 printf 为例)
hello.o 中的未解析调用:
hello 中的重定位结果:
PLT条目(0x4010a0):
GOT条目初始值:首次调用时,GOT[printf] 指向PLT中的解析代码,触发动态链接器加载 printf 的实际地址。
2. 数据地址重定位(以字符串常量为例)
hello.o 中的未解析地址:
hello 中的重定位结果:
字符串常量位置:.rodata 段的 0x402038 存储 "Hello %s %s %s\n"。
5.6 hello的执行流程
1. 动态加载阶段
入口函数:_start(动态链接器)
地址:0x00007ffff7fe4540(位于 ld-linux-x86-64.so.2)
作用:加载程序依赖的共享库(如 libc.so.6),初始化运行环境。
2. 程序启动阶段
入口函数:_start(hello程序)
地址:0x4010f0(.text段)
操作:调用 __libc_start_main,传递 main 函数地址。
调用:call *0x403fd8 ; 调用 __libc_start_main@GLIBC_2.34
3.主函数执行阶段
入口函数:main
地址:0x401125
执行流程:
(1)参数检查:
cmpl $0x5, %edi ; 检查 argc 是否为5
jne 0x40113e ; 跳转到错误处理分支
(2)循环逻辑:
循环计数器:%ebp 从0递增至9。
函数调用链:
函数调用 | 地址 | 作用 |
printf@plt | 0x4010a0 | 输出格式化字符串 |
atoi@plt | 0x4010c0 | 将字符串参数转换为整数 |
sleep@plt | 0x4010e0 | 休眠指定秒数 |
循环终止条件:cmpl $0x9, %ebp
jle 循环体
4. 中断处理阶段
触发条件:用户按下 Ctrl-C(发送 SIGINT 信号)。
调用链:
函数名 | 地址 | 作用 |
__GI___clock_nanosleep | 0x00007ffff7ceca7a | 内核级休眠实现 |
__GI___nanosleep | 0x00007ffff7cf9a27 | 调用 clock_nanosleep |
__sleep | 0x00007ffff7d0ec63 | 封装 nanosleep 系统调用 |
main | 0x401181 | 主函数中的 sleep 调用点 |
5. 程序终止阶段
正常终止: main 返回后,__libc_start_main 调用 exit 终止进程。
中断终止: 内核直接终止进程,释放资源。
5.7 Hello的动态链接分析
1. 动态链接前的GOT状态
在程序首次调用外部函数前,GOT条目指向PLT中的解析代码。
调试命令与输出:
printf@got.plt(0x404008)初始值为 0x00401040,指向 printf@plt+0x10
atoi@got.plt(0x404018)初始值为 0x00401060,指向 atoi@plt+0x10。
2. 动态链接后的GOT状态
首次调用函数后,动态链接器(ld.so)解析实际函数地址并更新GOT。
调试命令与输出:
printf@got.plt(0x404008)更新为0x7ffff7c60100,指向libc.so中的printf实际地址。
atoi@got.plt(0x404018)更新为 0x7ffff7c46660,指向libc.so中的atoi实际地址。
5.8 本章小结
本章分析了hello程序从目标文件到可执行文件的链接过程及其运行机制。链接通过符号解析、地址重定位和节合并生成ELF结构,关键段包括.text(代码)、.rodata(只读数据)和.got.plt(动态函数地址)。虚拟地址空间中,ELF段权限与内存映射一致,系统区域由内核独立管理。重定位修正函数调用至PLT,动态链接通过GOT实现延迟绑定:首次调用触发地址解析并更新GOT为实际地址。程序执行流程从动态加载器ld.so初始化开始,经_start调用main,循环输出信息后通过exit或信号终止。
第6章 hello进程管理
6.1 进程的概念与作用
进程是程序的执行实例,拥有独立的地址空间、代码、数据和系统资源
作用:
资源分配:CPU时间片、内存、文件句柄等由操作系统统一管理
隔离保护:进程间相互隔离,避免数据冲突或非法访问
并发执行:通过进程调度实现多任务并行
6.2 简述壳Shell-bash的作用与处理流程
作用:
命令解析:解析用户输入的命令(如 ./hello)。
进程管理:创建子进程执行程序(fork + execve),支持后台运行(&)。
环境管理:维护环境变量(如 PATH)和输入输出重定向(>、<)。
处理流程:
1.读取输入:从终端或脚本获取命令。
2.解析命令:分割参数,处理管道(|)、重定向等。
3.执行命令:若为内置命令(如 cd),直接执行;否则创建子进程运行程序。
6.3 Hello的fork进程创建过程
调用fork():Shell创建子进程,复制父进程的地址空间、文件描述符等
返回值区分:
父进程:fork() 返回子进程PID
子进程:fork() 返回0
子进程任务:调用 execve() 加载 hello 程序,替换当前进程映像
6.4 Hello的execve过程
功能:加载并执行 hello 程序,替换当前进程的代码段、数据段和堆栈
参数传递:
argv:命令行参数数组(["学号", "姓名", ...])
envp:环境变量数组(如 PATH=/usr/bin)
执行后:
进程的入口点变为 hello 的 _start 函数
6.5 Hello的进程执行
进程上下文:包括寄存器值、程序计数器(PC)、栈指针(SP)等。
时间片调度:操作系统分配CPU时间片(如10ms),时间耗尽后触发时钟中断,切换进程。
用户态与核心态转换:
系统调用(如 sleep()):从用户态切换到核心态,执行内核代码。
中断处理(如 Ctrl-C):内核接管控制权,处理信号后返回用户态。
6.6 hello的异常与信号处理
异常类型 | 触发信号 | 处理方式 |
中断(Ctrl-C) | SIGINT | 终止进程 |
停止(Ctrl-Z) | SIGTSTP | 挂起进程至后台 |
非法操作 | SIGSEGV | 终止进程并生成核心转储 |
1.Ctrl-Z 挂起
2.ps
3.jobs
4.pstree
5.kill
6.fg & Ctrl+C
6.7本章小结
本章探讨了hello程序的进程管理机制。进程作为程序的执行实例,为hello提供了资源分配、隔离保护和并发执行的能力。Shell(bash)通过解析用户命令、创建子进程(fork)并加载程序(execve)来管理hello的执行。fork复制父进程环境,而execve将hello的代码和数据载入内存,开启新的执行流程。进程运行时,操作系统通过时间片调度和上下文切换实现多任务并行,并在系统调用或中断时切换用户态与核心态。此外,hello需处理异常和信号,如SIGINT终止进程、SIGTSTP挂起进程,并通过ps、jobs等工具进行监控和管理。本章揭示了进程从创建到终止的全生命周期,体现了操作系统对程序执行的核心控制作用。
第7章 hello的存储管理
7.1 hello的存储器地址空间
在hello程序的执行过程中,存储器地址通过多级抽象实现高效管理:
逻辑地址:程序段内的偏移地址,由编译器和链接器生成,对应代码段或数据段的局部位置。
hello的main函数在代码段中的逻辑地址为0x4004d6,表示相对于代码段基址的偏移。
线性地址:Intel处理器通过段式管理将逻辑地址转换为全局线性地址(段基址 + 偏移)。若hello的代码段基址为0x08048000,逻辑地址0x100转换为线性地址0x08048100。
虚拟地址:hello进程看到的独立地址空间(0x8048000),通过页表映射到物理地址,实现进程间隔离。
物理地址:实际内存芯片上的硬件地址,由MMU通过页表转换得到。
当hello执行printf时,指令的逻辑地址(如0x4010a0)经段式管理转换为线性地址,再通过页式管理映射到物理地址,最终访问内存中的字符串常量(.LC1位于.rodata段)。
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel使用段式管理完成逻辑地址到线性地址的转换:
段选择符:代码段寄存器(CS)存储段选择符,指向全局描述符表(GDT)中的段描述符。
段描述符:包含段基址、界限和权限。hello的代码段基址为0x08048000。
线性地址计算:线性地址 = 段基址 + 逻辑偏移量。
hello的main函数逻辑地址0x4004d6,在段基址0x08048000下转换为线性地址0x0804d4d6(假设段基址为0x08048000)。
7.3 Hello的线性地址到物理地址的变换-页式管理
操作系统通过页式管理将虚拟地址(线性地址)映射到物理地址:
分页机制:虚拟地址划分为页号和页内偏移
页表查询:通过页表基址寄存器找到页表,逐级查询四级页表项(PML4→PDPT→PD→PT),获取页框。
物理地址生成:物理地址 = PFN << 12 | Offset
假设hello访问虚拟地址0x402038(存储"Hello %s %s %s\n"),页号为0x402,页内偏移0x038,通过页表查询PFN0x2000,物理地址为0x2000038。
7.4 TLB与四级页表支持下的VA到PA的变换
现代系统通过TLB和四级页表加速地址转换:
四级页表:虚拟地址划分为4级索引(如9+9+9+9+12位),逐级查询页表项
TLB缓存:转换后备缓冲器(TLB)缓存近期页表项。若命中,直接获取PFN;否则遍历四级页表
当hello首次访问0x4010f0(_start函数入口)时,TLB未命中,需4次内存访问完成页表遍历。后续访问同一页时,TLB命中,转换延迟降至1~2周期
7.5 三级Cache支持下的物理内存访问
物理内存访问通过三级Cache优化性能:
缓存层级:L1(指令/数据分离)、L2(统一)、L3(共享)缓存,逐级容量增大、速度降低。
缓存行匹配:物理地址被划分为标记(Tag)、组索引(Set Index)和块偏移(Offset),通过组相联映射定位缓存行。
替换策略:LRU(最近最少使用)算法淘汰旧数据。
hello访问物理地址0x2000038时,若L1 Cache命中(Tag匹配),1~2周期完成读取;否则触发L2/L3访问(约10~40周期)或内存加载(约100周期)。
7.6 hello进程fork时的内存映射
fork创建子进程时采用写时复制(Copy-on-Write)机制:
共享父进程页表:子进程初始共享父进程的物理页,页表项标记为只读。
写时复制:当任一进程尝试修改共享页时,触发页错误,内核分配新物理页并复制内容。
若父进程修改hello的全局变量,子进程将获得独立的物理页副本,保证进程隔离性。
7.7 hello进程execve时的内存映射
execve加载hello程序时重建内存映射:
释放旧地址空间:清除原进程的代码、数据段和堆栈。
加载新程序:将hello的代码段映射到0x8048000,数据段到0x8050000,并初始化堆栈和堆。
动态链接:若使用共享库(如libc.so),通过动态链接器映射到进程空间。
execve后,hello的.text段映射到0x4010f0(可执行代码),.rodata段到0x402000(只读字符串),权限为R-X和R--。
7.8 缺页故障与缺页中断处理
触发条件:访问未加载的虚拟页(页表项无效)或写只读页(如COW场景)。
中断处理:检查虚拟地址合法性。
分配物理页(或从磁盘换入)。
更新页表项并重新执行指令。
hello首次访问堆内存时触发缺页,内核分配零页(全零填充的物理页),避免未初始化数据错误。
7.9动态存储分配管理
printf调用malloc时,动态内存管理策略如下:
隐式空闲链表:通过块头部的大小字段遍历空闲块,合并相邻空闲块减少碎片。
显式空闲链表:维护空闲块链表,加速分配。
伙伴系统:按2的幂划分块,减少外部碎片但可能产生内部碎片。
glibc的malloc为hello分配小内存时,优先从线程本地缓存(tcache)获取,减少锁竞争;大内存则通过mmap系统调用直接映射。
7.10本章小结
本章系统解析了hello程序的存储管理机制。从逻辑地址到物理地址的转换依赖段式与页式管理,TLB和四级页表优化了映射效率。三级Cache通过层级缓存降低内存访问延迟。进程fork时的写时复制与execve的内存重建机制,保障了进程的隔离与灵活性。缺页中断实现按需加载,动态内存管理策略(如malloc)平衡了效率与碎片问题。存储管理是操作系统资源调度的核心,支撑了hello程序从地址抽象到物理执行的全流程。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux采用统一的“一切皆文件”模型管理IO设备:
设备抽象:所有硬件设备(键盘、显示器、磁盘等)被抽象为文件,位于/dev目录下,如/dev/tty表示终端设备。
设备分类:
字符设备:以字节流形式访问(如键盘、串口),通过字符设备文件(如/dev/input/event0)操作。
块设备:以固定大小数据块访问(如硬盘、SSD),通过块设备文件(如/dev/sda)操作。
设备驱动管理:内核通过设备驱动程序与硬件交互,用户程序通过标准Unix IO接口(如read、write)访问设备,无需关心底层实现。
hello通过printf输出到终端时,实际是向字符设备文件/dev/tty写入数据,由显示驱动处理;getchar则从同一设备读取键盘输入。
8.2 简述Unix IO接口及其函数
Unix IO接口提供以下核心系统调用:
open:打开设备文件(如/dev/tty) 返回文件描述符(fd)。
read:从fd读取数据(如键盘输入) 函数原型:ssize_t read(int fd, void *buf, size_t count)
write:向fd写入数据(如屏幕输出) 函数原型:ssize_t write(int fd, const void *buf, size_t count)
close:关闭fd,释放资源
ioctl:控制设备参数(如设置终端模式)
IO模式:
阻塞IO:默认模式,若设备无数据,进程挂起等待(如getchar等待键盘输入)。
非阻塞IO:通过O_NONBLOCK标志设置,立即返回错误码EAGAIN。
异步IO:通过信号或回调通知数据就绪(如aio_read)。
8.3 printf的实现分析
1.格式化字符串处理:调用vsprintf将格式字符串和参数转换为格式化后的字符串(如"Hello 学号 姓名 手机号 1"),存入用户态缓冲区。
2.系统调用写入:调用write系统调用(函数号SYS_write),触发软中断(如syscall指令或int 0x80),切换到内核态。内核通过终端设备驱动将字符串写入显示缓冲区(vram)。
3.显示驱动处理:字模库映射将ASCII字符转换为像素点阵(16×16点阵),vram更新将像素RGB值写入显存对应位置(每个像素占4字节,格式为ARGB)。
屏幕刷新,显示控制器按刷新率(如60Hz)逐行读取vram,通过信号线输出到显示器。
8.4 getchar的实现分析
1.键盘中断触发:用户按下按键时,键盘控制器发送中断请求(IRQ1),CPU调用键盘中断处理程序(ISR)。
2.扫描码转换:ISR读取键盘扫描码(如回车键为0x1C),转换为ASCII码(如'\n'),存入内核缓冲区tty_read_buf。
3.系统调用读取:getchar调用read系统调用(fd=0),从缓冲区读取ASCII码,若缓冲区为空则阻塞等待。遇到换行符('\n')时,read返回缓冲区内容,用户程序继续执行。
8.5本章小结
本章探讨了hello程序的IO管理机制。Linux通过“一切皆文件”模型将硬件设备抽象为字符设备(如终端/dev/tty)或块设备,用户程序通过标准Unix接口(如read、write)实现设备访问。printf的执行分为三步:vsprintf格式化字符串生成输出内容,write系统调用将数据写入显存(vram),显示驱动将ASCII字符转换为像素矩阵并刷新屏幕。getchar依赖键盘中断机制,中断处理程序将扫描码转为ASCII码存入内核缓冲区,read系统调用阻塞读取直至回车触发返回。
结论
Hello程序的生命周期完整体现了计算机系统的核心设计理念。从代码(Program)到进程(Process)的P2P流程中,编译系统通过预处理、编译、汇编、链接将高级语言转换为可执行文件;操作系统通过进程管理(fork、execve)和存储管理(段页式转换、TLB、Cache)为程序提供执行环境;硬件与内核协作完成指令执行、地址转换和IO操作。
通过本项目,我深切感悟到:
分层抽象:计算机系统通过编译链、操作系统和硬件的分层抽象,实现了从高级语言到物理硬件的无缝衔接,极大提升了开发效率与系统可靠性。
动态性与灵活性:动态链接、写时复制和按需分页等机制,平衡了资源利用率与性能,展现了系统设计的精巧。
优化空间:在工具链配置(如编译器优化选项)、内存管理(如减少缺页中断)和IO效率(如缓冲策略)等方面仍有优化潜力。
本项目不仅巩固了计算机系统核心知识,更启发了对系统设计与实现的创新思考。
附件
文件名 | 作用 |
hello.c | |
hello.i | 预处理后的代码文件,展开所有宏、头文件,并删除注释。 |
hello.s | 汇编代码文件,包含x86-64汇编指令。 |
hello.o | 可重定位目标文件(ELF格式),尚未链接。 |
hello | 最终的可执行文件,包含动态链接库(如libc)。 |
hello_o_dump.txt | hello.o的反汇编输出(objdump -d), 用于分析机器码和汇编指令的对应关系。 |
hello_o_elf.txt | hello.o的ELF结构分析(readelf), 查看节头、符号表、重定位信息等。 |
hello_dump.txt | hello的反汇编输出(objdump -d), 分析可执行文件的机器码和运行时行为。 |
hello_elf.txt | hello的ELF结构分析(readelf), 查看程序入口、动态链接信息等。 |
原始的C语言源代码文件 |
预处理后的代码文件,展开所有宏、头文件,并删除注释。 |
参考文献
[1] Randal E. Bryant ,David O'Hallaron. 深入理解计算机系统(第3版), 机械工业出版社,2016.11
[2] Alfred V. Aho,Monica S.Lam,Ravi Sethi,Jeffrey D. Ullman. 编译原理, 机械工业出版社,2008.12
[3] Andrew S. Tanenbaum. 现代操作系统(第3版),机械工业出版社,2009.7