本文通过一个简单的"Hello"程序,深入剖析了计算机系统中程序从源代码到进程执行的全过程。以hello.c程序为研究对象,按照P2P(From Program to Process)和O2O(From Zero-0 to Zero-0)的生命周期主线,系统分析了程序在Linux系统中的预处理、编译、汇编、链接、加载、执行和终止等各个阶段。实验采用GCC工具链在Ubuntu 20.04环境下进行,通过一系列系统工具(gcc、gdb、readelf、objdump、strace等)追踪和分析程序的转换过程。研究内容涵盖了ELF文件格式、虚拟内存管理、进程调度、动态链接、I/O管理等多个计算机系统核心概念。实验结果表明,一个简单的C程序背后涉及复杂的系统机制,包括多级存储管理、地址转换、异常处理和设备抽象等。本文不仅验证了计算机系统课程的理论知识,还通过实践加深了对系统层次结构和各组件协同工作的理解,为深入学习和研究计算机系统提供了实践基础。
关键词:计算机系统;程序生命周期;ELF格式;虚拟内存;进程管理;I/O管理
目 录
6.2 简述壳Shell-bash的作用与处理流程 - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 12 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 12 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 12 -
7.7 hello进程execve时的内存映射 - 12 -
第1章 概述
1.1 Hello简介
Hello程序的生命周期可以从两个维度理解:P2P(From Program to Process)和O2O(From Zero-0 to Zero-0)。
一、P2P(From Program to Process):Hello程序始于一个简单的C语言源代码文件hello.c。这是一个静态的文本文件,包含程序逻辑和预处理指令。通过GCC编译器工具链,它经历了四个关键阶段:
1、预处理:预处理器处理#include、#define等指令,展开头文件,生成hello.i。
2、编译:编译器将预处理后的C代码转换为汇编代码,生成hello.s。
3、汇编:汇编器将汇编代码转换为机器指令,生成可重定位目标文件hello.o。
4、链接:链接器将hello.o与必要的库文件(如libc.so)链接,生成可执行文件hello。
当用户在shell中输入./hello 1234567890 张三 13812345678 2时,shell通过fork()创建子进程,再通过execve()加载hello可执行文件,将磁盘上的静态程序转化为内存中动态执行的进程。
二、O2O(From Zero-0 to Zero-0):进程从"零"(不存在)开始其生命周期:
1、进程创建:Shell通过系统调用为hello创建进程控制块(PCB),分配虚拟地址空间,加载代码和数据段。
2、进程执行:进程执行main函数,检查参数数量,循环10次输出格式化字符串,每次间隔指定秒数,最后等待用户输入。
3、进程终止:用户按回车后,进程执行return 0,通过exit()系统调用终止。
4、资源回收:操作系统回收进程占用的所有资源(内存、文件描述符、PCB等),进程状态回归为"零"。
在整个生命周期中,Hello进程与用户(通过键盘输入和屏幕输出)、操作系统内核(通过系统调用)、C标准库等对象进行交互。
1.2 环境与工具
1、硬件环境:
CPU:Intel(R) Core(TM) i7-14650HX处理器,支持x86-64指令集架构
内存:3.8GB RAM,支持虚拟内存管理
2、软件环境:
操作系统:Ubuntu 20.04.4 LTS,Linux内核版本5.x
Shell环境:zsh 5.8,提供命令行交互界面
3、开发与调试工具:
GCC编译器:版本9.4.0,用于程序编译和链接
GNU调试器(GDB):版本9.2,用于程序调试和反汇编分析
binutils工具集:包含readelf、objdump、size等二进制分析工具
文本编辑器:vim或Visual Studio Code,用于代码编写
4、特殊编译选项:
为简化分析和调试,Hello程序使用以下编译选项:
-m64:生成64位代码
-Og:优化调试体验
-no-pie:禁用位置无关可执行文件
-fno-stack-protector:禁用栈保护
-fno-PIC:禁用位置无关代码
1.3 中间结果
在Hello程序的编译和执行过程中,生成以下中间结果文件:
| 文件名 | 生成命令 | 文件作用 |
| hello.i | gcc -E hello.c -o hello.i | 预处理后的源代码文件,包含所有展开的头文件 |
| hello.s | gcc -S hello.c -o hello.s | 汇编语言文件,包含x86-64汇编指令 |
| hello.o | gcc -c hello.c -o hello.o | 可重定位目标文件,包含机器指令但未解析外部引用 |
| hello | gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC hello.c -o hello | 可执行目标文件,包含完全解析的机器代码 |
这些文件记录了Hello程序从源代码到可执行文件的完整转换过程,为后续分析提供了重要依据。
1.4 本章小结
本章概述了Hello程序的完整生命周期,从P2P(程序到进程)的静态转换过程,到O2O(从零到零)的动态执行过程。介绍了实验所需的硬件环境、软件平台和开发工具,列出了编译过程中生成的关键中间文件。这些基础工作为后续章节深入分析Hello程序的预处理、编译、汇编、链接、加载和执行等各个阶段奠定了坚实基础。通过本实验,我们将全面理解一个简单C程序在计算机系统中的完整执行路径,掌握计算机系统层次结构的关键概念。
第2章 预处理
2.1 预处理的概念与作用
预处理是C语言编译过程的第一阶段,由预处理器(cpp)执行。其主要作用是对源代码进行文本级别的转换和处理,为后续的编译阶段做准备。预处理的主要功能包括:
1、文件包含:处理#include指令,将被引用头文件的内容完整地插入到源文件中。
2、宏展开:处理#define定义的宏,进行文本替换。
3、条件编译:根据#if、#ifdef、#ifndef、#else、#elif、#endif等指令,选择性地包含或排除代码段。
4、注释删除:移除源代码中的所有注释,包括单行注释(//)和多行注释(/* */)。
5、行号标记:添加#line指令,保留原始文件的行号信息,便于调试和错误定位。
6、其他指令:处理#pragma、#error等特殊指令。
预处理后的文件是一个纯文本文件,包含了所有必要的声明和定义,但尚未进行语法分析和语义分析。
2.2在Ubuntu下预处理的命令
在Ubuntu系统中,可以使用GCC的-E选项执行预处理操作:
gcc -E hello.c -o hello.i
执行命令后,预处理器读取hello.c文件,处理其中的预处理指令,生成hello.i文件。该文件大小为64KB(64,756字节),包含3061行代码,相比原始的hello.c文件(592字节,25行)显著增大,这主要是因为标准库头文件的内容被完整展开。

2.3 Hello的预处理结果解析
通过分析hello.i文件,可以观察到预处理的具体效果:
1、头文件展开:原始的hello.c文件包含三个头文件:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
预处理后,这些头文件的内容被完全展开。通过统计发现,hello.i文件中包含279个头文件包含标记,表明预处理过程中涉及了大量的嵌套包含关系。主要展开的头文件包括:
/usr/include/stdio.h:标准输入输出函数声明
/usr/include/stdlib.h:标准库函数声明(如exit)
/usr/include/unistd.h:Unix标准函数声明(如sleep)
以及这些头文件所依赖的其他头文件
2、注释删除:原始代码中的所有注释都被预处理器删除:文件开头的多行注释(说明编译选项和用法的部分)被完全移除;行内注释也被删除,只保留有效的代码
3、行号标记添加:预处理器在hello.i文件中添加了大量的行号标记,格式为:# 行号 "文件名" [标志]
例如:
# 1 "hello.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 10 "hello.c" 2
这些标记记录了代码的原始位置,便于编译器生成调试信息和定位编译错误。
4、main函数保留:对比预处理前后的main函数可以发现,预处理主要影响的是头文件部分,而main函数本身的逻辑结构保持不变:
预处理前的main函数(来自hello.c):
int main(int argc,char *argv[]){
int i;
if(argc!=5){
printf("用法: Hello 学号 姓名 手机号 秒数!\n");
exit(1);
}
for(i=0;i<10;i++){
printf("Hello %s %s %s\n",argv[1],argv[2],argv[3]);
sleep(atoi(argv[4]));
}
getchar();
return 0;
}
预处理后的main函数(来自hello.i第3048行开始):
int main(int argc,char *argv[]){
int i;
if(argc!=5){
printf("用法: Hello 学号 姓名 手机号 秒数!\n");
exit(1);
}
for(i=0;i<10;i++){
printf("Hello %s %s %s\n",argv[1],argv[2],argv[3]);
sleep(atoi(argv[4]));
}
getchar();
return 0;
}
唯一的变化是缩进格式略有不同(制表符被替换为空格),这是预处理器的正常行为。
5、宏定义展开:虽然hello.c中没有显式的#define宏定义,但标准库头文件中包含大量的宏定义。预处理器会展开这些宏,例如NULL、EOF、BUFSIZ等常量宏,以及各种函数宏。
2.4 本章小结
本章通过实际操作分析了Hello程序的预处理阶段。预处理是C程序编译的第一步,它将分散在多个文件中的代码和声明整合到一个文件中。通过gcc -E命令生成hello.i文件,可以直观地看到预处理的效果:头文件被展开、注释被删除、行号标记被添加。预处理后的文件包含了程序所需的所有声明和定义,为后续的编译阶段做好了准备。hello.i文件从原始的592字节扩展到64KB,这充分展示了C语言标准库的复杂性和模块化设计思想。预处理器的正确处理确保了编译器能够获得完整的程序上下文,进行正确的语法分析和代码生成。
第3章 编译
3.1 编译的概念与作用
编译是将预处理后的高级语言源代码转换为汇编语言代码的过程。编译器(cc1)对.i文件进行以下处理:
1、词法分析:将源代码分解为有意义的标记(tokens),如关键字、标识符、常量等。
2、语法分析:根据C语言的语法规则构建抽象语法树(AST)。
3、语义分析:检查类型一致性、变量声明等语义规则。
4、中间代码生成:生成与机器无关的中间表示(如三地址码)。
5、代码优化:对中间代码进行各种优化,提高执行效率。
6、目标代码生成:将优化后的中间代码转换为目标机器的汇编代码。
对于Hello程序,编译器将hello.i文件转换为x86-64架构的汇编代码hello.s,为后续的汇编阶段做准备。
3.2 在Ubuntu下编译的命令
在Ubuntu系统中,可以使用GCC的-S选项执行编译操作:
gcc -S hello.c -o hello.s
执行命令后,编译器生成hello.s文件,该文件包含83行汇编代码,大小为1,426字节。文件类型为"assembler source, ASCII text",表明这是可读的汇编语言源代码。

3.3 Hello的编译结果解析
通过分析hello.s文件,可以深入了解编译器如何将C语言代码转换为x86-64汇编指令。
1、程序结构和节区:hello.s文件包含以下主要部分:
.file指令:标识源文件名
.text节:存放可执行代码
.section .rodata节:存放只读数据(字符串常量)
.globl main:声明main为全局符号
.type main, @function:定义main为函数类型
2、字符串常量处理:编译器将字符串常量存储在.rodata(只读数据)节中:
.LC0:
.string "用法: Hello 学号 姓名 手机号 秒数!"
.LC1:
.string "Hello %s %s %s\n"
注意:.LC0中的中文字符被转换为UTF-8编码的转义序列,这是汇编器处理Unicode字符的正常方式。
3、函数栈帧建立:main函数开始时建立栈帧:
main:
pushq %rbp ; 保存旧的基指针
movq %rsp, %rbp ; 设置新的基指针
subq $32, %rsp ; 分配32字节栈空间
栈帧布局如下:
-4(%rbp):局部变量i(4字节)
-20(%rbp):参数argc(4字节)
-32(%rbp):参数argv(8字节,指针)
剩余空间用于对齐和临时存储
4、参数检查和错误处理:对应C代码if(argc!=5)的汇编实现:
cmpl $5, -20(%rbp) ; 比较argc和5
je .L2 ; 如果相等跳转到.L2
leaq .LC0(%rip), %rdi ; 加载错误信息地址
call puts@PLT ; 调用puts输出
movl $1, %edi ; 设置退出码为1
call exit@PLT ; 调用exit退出
5、循环结构实现:
循环初始化(对应for(i=0;i<10;i++)):
.L2:
movl $0, -4(%rbp) ; i = 0
jmp .L3 ; 跳转到循环条件检查
循环体(对应printf和sleep调用):
.L4:
; 准备printf参数
movq -32(%rbp), %rax ; argv基地址
addq $24, %rax ; argv[3]地址(偏移24字节)
movq (%rax), %rcx ; argv[3]值 -> rcx(第4个参数)
movq -32(%rbp), %rax
addq $16, %rax ; argv[2]地址(偏移16字节)
movq (%rax), %rdx ; argv[2]值 -> rdx(第3个参数)
movq -32(%rbp), %rax
addq $8, %rax ; argv[1]地址(偏移8字节)
movq (%rax), %rax ; argv[1]值 -> rax
movq %rax, %rsi ; 移动到rsi(第2个参数)
leaq .LC1(%rip), %rdi ; 格式化字符串地址 -> rdi(第1个参数)
movl $0, %eax ; 设置浮点参数数量为0
call printf@PLT ; 调用printf
; 调用sleep(atoi(argv[4]))
movq -32(%rbp), %rax
addq $32, %rax ; argv[4]地址(偏移32字节)
movq (%rax), %rax
movq %rax, %rdi ; argv[4]字符串 -> rdi
call atoi@PLT ; 转换为整数
movl %eax, %edi ; 结果 -> edi
call sleep@PLT ; 调用sleep
addl $1, -4(%rbp) ; i++
循环条件检查:
.L3:
cmpl $9, -4(%rbp) ; 比较i和9
jle .L4 ; 如果i <= 9跳转到.L4(循环体)
注意:编译器将i<10优化为i<=9,这是常见的优化策略。
6、函数调用约定:x86-64 System V ABI调用约定:
前6个整数/指针参数通过寄存器传递:rdi, rsi, rdx, rcx, r8, r9
额外参数通过栈传递
返回值存储在rax寄存器中
al寄存器用于指示使用的向量寄存器数量(对printf很重要)
7、函数返回:程序结束部分:
call getchar@PLT ; 等待用户输入
movl $0, %eax ; 返回值0 -> eax
leave ; 恢复栈帧
ret ; 返回
3.4 本章小结
本章详细分析了Hello程序的编译过程。编译器将C语言源代码转换为x86-64汇编代码,展示了高级语言构造与底层机器指令之间的映射关系。通过分析hello.s文件,我们观察到:编译器如何管理栈帧和局部变量、如何实现控制流结构(条件判断和循环)、如何处理函数调用和参数传递、如何存储字符串常量等重要概念。编译器的优化策略也在代码中有所体现,如循环条件的优化。这些分析为理解计算机系统如何执行高级语言程序提供了重要基础,也为后续的汇编和链接阶段做好了准备。
第4章 汇编
4.1 汇编的概念与作用
汇编是将汇编语言源代码(.s文件)转换为机器语言目标文件(.o文件)的过程。汇编器(as)执行以下主要任务:
1、指令编码:将助记符形式的汇编指令转换为二进制机器码。
2、符号解析:处理标签和符号,为它们分配临时的地址或偏移量。
3、数据编码:将数据定义(如.string、.word)转换为二进制形式。
4、生成重定位信息:记录需要链接器处理的符号引用位置。
5、生成节区:按照指令类型组织代码和数据到不同的ELF节区。
汇编过程生成了可重定位目标文件,该文件包含机器代码但尚未解决外部引用,为后续的链接阶段做好准备。
4.2 在Ubuntu下汇编的命令
在Ubuntu系统中,可以使用GCC的-c选项执行汇编操作:
gcc -c hello.c -o hello.o
执行命令后,汇编器生成hello.o文件,该文件大小为2.2KB,包含可重定位的机器代码。文件类型为"ELF 64-bit LSB relocatable",表明这是一个可重定位目标文件。

4.3 可重定位目标elf格式
1、ELF头部分析:通过readelf -h hello.o查看ELF头部:
文件类型:REL(可重定位文件)
架构:x86-64
节区头部表:包含14个节区,起始于偏移量0x4f0
入口点地址:0(可重定位文件无入口点)
2、节区头部表分析:通过readelf -S hello.o查看主要节区:
| 节区名称 | 类型 | 虚拟地址 | 大小 | 标志 | 说明 |
| .text | PROGBITS | 0x0 | 0x9d | AX | 可执行代码,包含main函数 |
| .rodata | PROGBITS | 0x0 | 0x40 | A | 只读数据,包含字符串常量 |
| .data | PROGBITS | 0x0 | 0x0 | WA | 已初始化数据(此处为空) |
| .bss | NOBITS | 0x0 | 0x0 | WA | 未初始化数据(此处为空) |
| .rela.text | RELA | 0x0 | 0xc0 | I | .text节的重定位信息 |
| .symtab | SYMTAB | 0x0 | 0x1b0 | 符号表 |
3、重定位项目分析:通过readelf -r hello.o查看重定位信息,有8个重定位条目:
| 偏移量 | 类型 | 符号 | 说明 |
| 0x1c | R_X86_64_PC32 | .rodata-4 | 引用字符串".LC0"(错误信息) |
| 0x21 | R_X86_64_PLT32 | puts | 调用puts函数 |
| 0x2b | R_X86_64_PLT32 | exit | 调用exit函数 |
| 0x5f | R_X86_64_PC32 | .rodata+0x2c | 引用字符串".LC1"(格式化字符串) |
| 0x69 | R_X86_64_PLT32 | printf | 调用printf函数 |
| 0x7c | R_X86_64_PLT32 | atoi | 调用atoi函数 |
| 0x83 | R_X86_64_PLT32 | sleep | 调用sleep函数 |
| 0x92 | R_X86_64_PLT32 | getchar | 调用getchar函数 |
这些重定位条目记录了所有需要链接器修正的位置。
4.4 Hello.o的结果解析
1、反汇编代码分析:通过objdump -d hello.o查看反汇编代码,main函数从地址0开始,共157字节(0x9d)。关键部分解析:
# 栈帧建立
0: f3 0f 1e fa endbr64 ; 控制流强制结束指令
4: 55 push %rbp ; 保存旧基指针
5: 48 89 e5 mov %rsp,%rbp ; 设置新基指针
8: 48 83 ec 20 sub $0x20,%rsp ; 分配32字节栈空间
# 参数存储和条件判断
c: 89 7d ec mov %edi,-0x14(%rbp) ; 存储argc
f: 48 89 75 e0 mov %rsi,-0x20(%rbp) ; 存储argv
13: 83 7d ec 05 cmpl $0x5,-0x14(%rbp) ; 比较argc和5
17: 74 16 je 2f <main+0x2f> ; 相等则跳转
# 错误处理(需要重定位)
19: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi ; 加载LC0地址(占位符)
20: e8 00 00 00 00 callq 25 <main+0x25> ; 调用puts(占位符)
2、机器语言与汇编语言的映射关系:比较hello.s和hello.o的反汇编:
| 汇编代码 | 机器代码 | 说明 |
| subq $32, %rsp | 48 83 ec 20 | 操作码+立即数编码 |
| movl %edi, -20(%rbp) | 89 7d ec | 寄存器-内存传输 |
| call puts@PLT | e8 00 00 00 00 | call指令+占位符 |
3、需要重定位的指令分析:所有外部引用都使用占位符:
函数调用:e8 00 00 00 00(call指令),操作码e8后跟32位相对偏移占位符
数据引用:48 8d 3d 00 00 00 00(lea指令),加载地址占位符
这些占位符将在链接时被替换为实际地址。
4、字符串常量分析:通过objdump -s -j .rodata hello.o查看.rodata节:
偏移0x00:中文字符串"用法: Hello 学号 姓名 手机号 秒数!"的UTF-8编码
偏移0x30:格式化字符串"Hello %s %s %s\n"
5、符号表分析:通过readelf -s hello.o查看符号表:
main:已定义,类型为FUNC,大小157字节
puts、exit、printf、atoi、sleep、getchar:未定义(UND)需要在链接时解析
4.5 本章小结
本章详细分析了Hello程序的汇编过程。汇编器将汇编代码hello.s转换为可重定位目标文件hello.o。通过分析ELF格式,我们了解到.o文件包含多个节区:.text(代码)、.rodata(只读数据)、以及重定位和符号信息。反汇编显示,所有外部引用(函数调用和全局数据)都使用占位符,需要通过重定位在链接时修正。字符串常量被编码为UTF-8格式存储在.rodata节。符号表显示main函数已定义,而库函数均为未定义状态。这些分析揭示了可重定位目标文件的结构和内容,为理解链接过程奠定了基础。汇编阶段完成了从人类可读的汇编代码到机器可执行的二进制代码的转换,但最终的地址绑定和外部引用解析需要链接器完成。
第5章 链接
5.1 链接的概念与作用
链接是将一个或多个可重定位目标文件合并成一个可执行目标文件的过程。链接器(ld)执行以下主要任务:
1、符号解析:将每个符号引用与一个符号定义关联起来。
2、重定位:将符号定义与内存地址关联,并修改所有对这些符号的引用。
3、节区合并:将来自不同目标文件的相同类型节区合并。
4、地址分配:为输出文件分配运行时内存地址。
5、库处理:解析对库函数的引用,提取所需模块。
对于Hello程序,链接器将hello.o与C标准库(libc.so)等必要的库文件链接,生成可执行文件hello。
5.2 在Ubuntu下链接的命令
使用ld的链接命令:
ld -o hello /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 -dynamic-linker /lib64/ld-linux-x86-64.so.2
执行命令后,链接器生成hello可执行文件,大小为17KB,类型为"ELF 64-bit LSB executable, dynamically linked"。

5.3 可执行目标文件hello的格式
1、ELF头部分析:通过readelf -h hello查看:
文件类型:EXEC(可执行文件)
入口点:0x4010f0(_start函数地址)
程序头表:13个程序头,描述如何加载到内存
节区头部表:31个节区
2、程序头表分析:通过readelf -l hello查看加载信息:
| 类型 | 虚拟地址 | 物理地址 | 文件大小 | 内存大小 | 标志 | 包含节区 |
| LOAD | 0x400000 | 0x400000 | 0x648 | 0x648 | R | 只读数据段 |
| LOAD | 0x401000 | 0x401000 | 0x2e5 | 0x2e5 | R E | 代码段 |
| LOAD | 0x402000 | 0x402000 | 0x198 | 0x198 | R | 只读数据段 |
| LOAD | 0x403e10 | 0x403e10 | 0x248 | 0x260 | RW | 读写数据段 |
3、关键节区分析
.interp:指定动态链接器路径 /lib64/ld-linux-x86-64.so.2
.plt:过程链接表,用于延迟绑定
.got.plt:全局偏移量表,存储动态符号的实际地址
.dynamic:动态链接信息
.dynsym:动态符号表
.rela.plt:PLT重定位表
5.4 hello的虚拟地址空间
通过程序头表分析,hello的虚拟地址空间布局如下:
1、代码段(0x401000-0x4012e5):包含可执行代码,包括_start、main、_init等函数
2、只读数据段1(0x400000-0x400648):包含ELF头部、程序头表、动态链接信息
3、只读数据段2(0x402000-0x402198):包含.rodata节,存储字符串常量
4、读写数据段(0x403e10-0x404070):包含.data、.bss、.got.plt等
运行时还会分配堆栈空间:
1、栈:从高地址向低地址增长,通常从0x7fffffffffff开始
2、堆:位于数据段之后,通过brk系统调用动态扩展
5.5 链接的重定位过程分析
1、重定位条目变化:对比hello.o和hello的重定位信息:
hello.o中的重定位(8个条目):
R_X86_64_PC32 .rodata
R_X86_64_PLT32 puts
R_X86_64_PLT32 exit
R_X86_64_PC32 .rodata+0x2c
R_X86_64_PLT32 printf
R_X86_64_PLT32 atoi
R_X86_64_PLT32 sleep
R_X86_64_PLT32 getchar
hello中的重定位(9个条目,类型不同):
R_X86_64_GLOB_DAT __libc_start_main
R_X86_64_GLOB_DAT __gmon_start__
R_X86_64_COPY stdin
R_X86_64_JUMP_SLOT puts
R_X86_64_JUMP_SLOT strtol
R_X86_64_JUMP_SLOT __printf_chk
R_X86_64_JUMP_SLOT exit
R_X86_64_JUMP_SLOT sleep
R_X86_64_JUMP_SLOT getc
2、重定位过程:链接器执行以下重定位操作:
1)合并节区:将各输入文件的.text、.data、.bss等节区合并
2)符号解析:
main:在hello.o中定义,分配地址0x4011d6
puts、printf等:在libc.so中定义,通过动态链接解析
3)地址计算:
字符串常量.LC0(错误信息):地址0x402008
字符串常量.LC1(格式化字符串):地址0x402038
4)引用修正:
将callq 25 <main+0x25>修正为callq 401090 <puts@plt>
将lea 0x0(%rip),%rdi修正为mov $0x402008,%edi
3、具体修正示例:以puts调用为例:
1)链接前(hello.o偏移0x20):e8 00 00 00 00(占位符)
2)链接后(hello地址0x401237):e8 54 fe ff ff
e8:call指令操作码
54 fe ff ff:相对偏移0xfffffe54(补码表示-428)
目标地址 = 0x401237 + 5 + (-428) = 0x401090(puts@plt)
5.6 hello的执行流程
通过gdb跟踪,hello的执行流程如下:
1、加载:操作系统加载hello,动态链接器ld-linux-x86-64.so.2解析依赖库
2、入口点:从0x4010f0(_start)开始执行
3、初始化:调用__libc_start_main,设置环境,注册退出处理函数
4、主函数:调用main(地址0x4011d6)
5、参数检查:比较argc与5,不相等则跳转到错误处理
6、循环输出:10次循环,每次调用__printf_chk输出,调用sleep等待
7、等待输入:调用getc等待用户输入
8、程序退出:返回0,通过exit系统调用终止
关键函数调用地址:
_start: 0x4010f0
main: 0x4011d6
puts@plt: 0x401090
__printf_chk@plt: 0x4010b0
exit@plt: 0x4010c0
5.7 Hello的动态链接分析
1、动态链接机制:hello使用动态链接,通过PLT(过程链接表)和GOT(全局偏移表)实现:
1)PLT结构:
0000000000401090 <puts@plt>:
401090: endbr64
401094: bnd jmpq *0x2f7d(%rip) # 跳转到GOT[2],地址0x404018
40109b: nopl 0x0(%rax,%rax,1)
2)GOT.plt内容:
0x00404000: 0x403e20 # .dynamic地址
0x00404010: 0x0 # 保留
0x00404018: 0x401030 # puts的GOT条目(初始指向PLT[0]的解析例程)
2、延迟绑定过程:第一次调用puts时的执行流程:
1)执行callq 401090 <puts@plt>
2)跳转到GOT[2](0x404018),初始指向PLT[0]的解析例程
3)动态链接器解析puts在libc中的实际地址
4)将实际地址写回GOT[2]
5)跳转到实际的puts函数
后续调用直接通过GOT跳转到实际函数,无需再次解析。
3、动态段分析:通过readelf -d hello查看动态段:
NEEDED:依赖库libc.so.6
INIT:初始化代码地址0x401000
PLTGOT:GOT地址0x404000
JMPREL:PLT重定位表地址0x4005b8
5.8 本章小结
本章详细分析了Hello程序的链接过程。链接器将可重定位目标文件hello.o与C标准库等链接,生成可执行文件hello。通过分析ELF格式,我们了解到可执行文件包含程序头表描述内存布局,以及多个用于动态链接的节区。链接过程完成了符号解析和重定位,为符号分配了具体的虚拟地址。动态链接机制通过PLT和GOT实现了延迟绑定,提高了程序启动效率。重定位分析显示,链接器将所有的外部引用修正为正确的地址,使程序能够在运行时正确执行。链接阶段是程序从模块化组件到完整可执行映像的关键转换,为程序加载和执行奠定了基础。
第6章 hello进程管理
6.1 进程的概念与作用
进程是计算机中正在执行的程序的实例,是操作系统进行资源分配和调度的基本单位。每个进程都有独立的地址空间、代码、数据和系统资源。进程的作用包括:
1、资源隔离:每个进程拥有独立的虚拟地址空间,防止进程间相互干扰。
2、并发执行:多个进程可以并发执行,提高系统资源利用率。
3、保护机制:操作系统通过进程机制保护系统资源和用户程序。
4、状态管理:进程具有创建、就绪、运行、阻塞、终止等状态。
对于Hello程序,当用户在shell中输入./hello命令时,操作系统为其创建一个新的进程,该进程独立于shell进程运行。
6.2 简述壳Shell-bash的作用与处理流程
Shell(本例中为zsh)是用户与操作系统内核之间的接口,主要作用包括:
1、命令解释:解析用户输入的命令。
2、进程创建:通过系统调用创建新进程执行命令。
3、环境管理:维护环境变量和进程工作目录。
4、输入输出重定向:处理管道、重定向等操作。
5、作业控制:管理前台和后台进程。
Shell处理流程:
1、读取命令:显示提示符,读取用户输入的命令行。
2、解析命令:将命令行分解为命令名和参数。
3、查找命令:在PATH环境变量指定的目录中查找可执行文件。
4、创建进程:通过fork()系统调用创建子进程。
5、执行程序:在子进程中通过execve()加载并执行程序。
6、等待完成:如果是前台进程,shell通过wait()等待其完成。
7、报告状态:显示命令执行结果和退出状态。
6.3 Hello的fork进程创建过程
当执行./hello 1234567890 张三 13812345678 1命令时:
1、fork()系统调用:Shell(PID: 5390)调用fork()创建子进程。
2、进程复制:操作系统复制父进程(shell)的地址空间、文件描述符表等资源。
3、返回PID:
父进程(shell)获得子进程的PID(如6731)
子进程获得0,表示自己是新创建的进程
4、进程独立性:子进程获得独立的进程控制块(PCB)、虚拟地址空间和资源。
5、执行程序:子进程调用execve()加载hello程序,替换当前内存映像。
实验验证:
# fork测试程序输出
子进程: PID=6777, PPID=6776
父进程: PID=6776, 创建的子进程PID=6777
6.4 Hello的execve过程
execve()系统调用将hello程序加载到进程地址空间:
1、参数传递:
execve("./hello", ["./hello", "1234567890", "张三", "13812345678", "1"], envp)
2、程序加载:
释放当前进程的代码段、数据段、堆栈
加载hello程序的ELF文件
建立新的代码段、数据段、堆栈段
3、内存映射:
代码段映射到0x401000
数据段映射到0x403e10
堆栈段动态分配
4、初始化:
设置程序计数器PC为入口地址0x4010f0(_start)
初始化寄存器状态
传递命令行参数和环境变量
strace跟踪显示:
execve("./hello", ["./hello", "1234567890", "\345\274\240\344\270\211", "13812345678", "1"], ...) = 0
6.5 Hello的进程执行
1、进程上下文:进程执行时需要维护的上下文信息包括:
寄存器状态:通用寄存器、程序计数器、栈指针等
内存状态:虚拟地址空间布局
文件描述符:打开的文件和I/O资源
信号处理:信号掩码和信号处理函数
资源限制:CPU时间、内存使用等限制
2、进程调度:Hello进程的执行过程涉及调度:
1)时间片分配:操作系统为hello进程分配CPU时间片(通常10-100ms)
2)上下文切换:
保存当前进程(如shell)的上下文
恢复hello进程的上下文
切换地址空间(CR3寄存器)
3)状态转换:
运行态→睡眠态:调用sleep()时
睡眠态→就绪态:sleep()时间到
就绪态→运行态:被调度器选中
3、用户态与核心态转换
Hello进程执行中的模式切换:
1)系统调用:sleep()、printf()、getchar()等触发从用户态到核心态的切换
2)中断处理:时钟中断、键盘中断等
3)异常处理:缺页异常、除零异常等
4)返回用户态:系统调用/中断处理完成后返回
6.6 hello的异常与信号处理
1、异常类型:Hello执行过程中可能出现的异常:
1)系统调用异常:int 0x80或syscall指令触发
2)缺页异常:访问未映射的虚拟地址
3)算术异常:除零错误(Hello中无此操作)
4)断点异常:gdb调试时设置断点
2、信号处理
Hello程序可能接收的信号及其处理:
| 信号 | 产生方式 | 默认处理 | 实验观察 |
| SIGINT | Ctrl+C | 终止进程 | 进程立即终止 |
| SIGTSTP | Ctrl+Z | 停止进程 | 进程挂起,状态变为T |
| SIGCONT | fg命令 | 继续运行 | 恢复挂起的进程 |
| SIGTERM | kill命令 | 终止进程 | 进程终止 |
| SIGCHLD | 子进程终止 | 忽略 | shell通过wait()回收 |
3、信号处理实验
实验1:正常执行和挂起
./hello 1234567890 张三 13812345678 1 &
# 输出:进程状态 S (sleeping)
实验2:SIGTSTP信号(Ctrl+Z)
kill -TSTP $HELLO_PID
# 输出:[2] + 6731 suspended (tty input)
# 进程状态变为 T (stopped)
实验3:SIGCONT信号
kill -CONT $HELLO_PID
# 进程恢复运行,继续输出
实验4:SIGTERM信号
kill -TERM $NEW_PID
# 输出:[3] + 6790 terminated
# 进程终止
实验5:进程状态查看
# 查看进程状态
ps -o pid,state,command -p $PID
# 输出:PID S COMMAND
# 6790 T ./hello ...
4、作业控制命令
jobs -l # 查看后台作业
fg %1 # 将作业1转到前台
bg %1 # 将作业1转到后台
kill %1 # 终止作业1
pstree -p $$ # 查看进程树
6.7本章小结
本章深入分析了Hello程序的进程管理机制。通过实验验证了shell如何通过fork()和execve()系统调用创建并执行hello进程。进程具有独立的地址空间和资源,通过上下文切换实现并发执行。Hello进程在执行过程中会经历多种状态转换,包括运行、睡眠、停止等。信号机制允许用户和系统控制进程行为,如暂停、继续、终止等操作。进程调度器通过时间片轮转确保公平性,内核通过用户态/核心态切换保护系统安全。这些机制共同构成了现代操作系统的进程管理基础,使得多个程序能够安全、高效地共享系统资源。
第7章 hello的存储管理
7.1 hello的存储器地址空间
通过/proc/$PID/maps分析,hello进程的虚拟地址空间布局如下:
逻辑地址、线性地址、虚拟地址、物理地址的概念:
1、逻辑地址:程序代码中使用的地址,由段选择符和段内偏移组成
2、线性地址:段式管理转换后的地址,在x86-64中通常等于虚拟地址
3、虚拟地址:进程视角的地址,通过页表映射到物理地址
4、物理地址:实际内存芯片上的地址
hello进程的地址空间布局:
0x00400000-0x00401000: 只读代码段(hello的.rodata)
0x00401000-0x00402000: 可执行代码段(hello的.text)
0x00402000-0x00404000: 只读数据段
0x00404000-0x00405000: 读写数据段(.data, .bss)
0x00bc6000-0x00be7000: 堆(动态分配内存)
0x7fa71f578000-0x7fa71f766000: libc共享库
0x7fff34b86000-0x7fff34ba7000: 栈
0x7fff34bde000-0x7fff34be0000: vdso(虚拟动态共享对象)
总虚拟内存大小:约2.5MB(2500KB),实际物理内存占用:约1.5MB(1496KB)
7.2 Intel逻辑地址到线性地址的变换-段式管理
在x86-64架构中,段式管理主要用于兼容性,实际使用扁平模型:
1、段描述符表:包含代码段、数据段、栈段等描述符
2、段选择符:16位,包含TI(表指示器)和RPL(请求特权级)
3、段基址:在x86-64中通常为0,实现扁平地址空间
4、转换公式:线性地址 = 段基址 + 偏移地址
对于hello程序:
1、代码段:基址0x0,界限0xffffffff,DPL=0
2、数据段:基址0x0,界限0xffffffff,DPL=0
3、栈段:基址0x0,界限0xffffffff,DPL=0
实际转换中,逻辑地址直接作为线性地址使用。
7.3 Hello的线性地址到物理地址的变换-页式管理
x86-64使用四级页表进行页式管理:
页表结构:
1、页大小:4KB(4096字节)
2、页表项大小:8字节
3、虚拟地址划分(48位地址空间):
位47-39:PGD索引(9位)
位38-30:PUD索引(9位)
位29-21:PMD索引(9位)
位20-12:PTE索引(9位)
位11-0:页内偏移(12位)
地址转换过程:
1、从CR3寄存器获取顶级页表(PGD)物理地址
2、用PGD索引在PGD中找到PUD地址
3、用PUD索引在PUD中找到PMD地址
4、用PMD索引在PMD中找到PTE地址
5、用PTE索引在PTE中找到物理页框地址
6、加上页内偏移得到物理地址
7.4 TLB与四级页表支持下的VA到PA的变换
TLB(转换后备缓冲区)作用:
缓存最近使用的虚拟地址到物理地址的映射,加速地址转换。
TLB访问流程:
1、TLB查找:用虚拟地址的页号部分查找TLB
2、TLB命中:直接获得物理页框地址
3、TLB未命中:
遍历四级页表获取物理地址
将映射关系存入TLB
可能触发TLB替换(LRU等算法)
hello程序中的TLB使用:
1、代码段访问:TLB缓存.text段的映射
2、数据段访问:TLB缓存.data、.rodata段的映射
3、堆访问:动态分配,TLB需要不断更新
4、栈访问:频繁访问,TLB命中率高
CPU信息显示支持pse(页大小扩展)、pae(物理地址扩展)等特性。
7.5 三级Cache支持下的物理内存访问
实验显示CPU缓存层次:
L1缓存:32KB数据缓存 + 32KB指令缓存
L2缓存:2MB(共享)
L3缓存:30MB(共享)
内存访问流程:
1、L1缓存查找:最快,约1-3周期
2、L2缓存查找:较慢,约10周期
3、L3缓存查找:更慢,约30-40周期
4、内存访问:最慢,约100-300周期
hello程序的内存访问模式:
1、顺序访问:代码执行,良好的空间局部性
2、栈访问:频繁但局部性强
3、堆访问:malloc/free操作,可能产生碎片
4、库函数访问:libc代码和数据,可能跨页
7.6 hello进程fork时的内存映射
fork()系统调用创建子进程时的内存管理:
写时复制(Copy-on-Write)机制:
1、页表复制:复制父进程页表,但指向相同的物理页
2、权限修改:将可写页标记为只读
3、写时复制:当任一进程尝试写入时:
触发缺页异常
内核分配新物理页
复制原页内容到新页
更新页表指向新页
恢复可写权限
实验验证:
// 父子进程地址相同,但物理页不同
父进程: 全局变量地址: 0x55f798a41010, 值: 100
子进程: 全局变量地址: 0x55f798a41010, 值: 100
子进程修改后: 全局变量: 101
父进程查看修改后: 全局变量: 100 // 值未改变,说明有独立的物理页
7.7 hello进程execve时的内存映射
execve()加载hello程序的内存映射过程:
映射步骤(strace跟踪显示):
1、brk(NULL):获取当前堆顶地址
2、mmap()调用序列:
mmap(NULL, 87641, PROT_READ, MAP_PRIVATE, 3, 0) // 映射程序头
mmap(NULL, 8192, PROT_READ|PROT_WRITE, ...) // 匿名映射(栈?)
mmap(NULL, 2037344, PROT_READ, ...) // 映射libc
mmap(0x7f7de7d35000, 1540096, PROT_READ|PROT_EXEC, ...) // libc代码段
mmap(0x7f7de7ead000, 319488, PROT_READ, ...) // libc数据段
3、mprotect():设置内存保护权限
4、munmap():解除不需要的映射
地址空间初始化:
1、代码段:映射到0x401000,权限r-xp
2、数据段:映射到0x404000,权限rw-p
3、堆:初始大小132KB,动态增长
4、栈:大小132KB,向低地址增长
7.8 缺页故障与缺页中断处理
缺页异常类型:
1、硬缺页:页不在内存中,需要从磁盘加载
2、软缺页:页在内存但未建立映射
3、写时复制缺页:尝试写入共享的只读页
缺页处理流程:
1、异常触发:CPU产生缺页异常(#PF)
2、保存现场:保存寄存器状态,切换到内核态
3、原因分析:检查错误代码和访问地址
4、页框分配:
如有空闲页框,直接分配
如无空闲,使用页面置换算法(如LRU)
5、数据加载:如需,从磁盘(交换区)加载数据
6、更新页表:建立虚拟地址到物理地址的映射
7、恢复执行:重新执行引发异常的指令
hello中的缺页场景:
1、程序启动:代码段和数据段初始加载
2、堆扩展:brk()系统调用扩展堆空间
3、栈增长:访问未映射的栈地址
4、动态链接:首次调用库函数
7.9动态存储分配管理
hello程序通过printf等函数间接使用动态内存分配:
malloc实现原理:
1、隐式空闲链表:通过大小字段连接空闲块
2、显式空闲链表:专门维护空闲块链表
3、分离空闲链表:按大小分类维护多个空闲链表
分配策略:
1、首次适应:从链表头开始查找第一个足够大的块
2、最佳适应:查找大小最接近需求的块
3、最差适应:总是分配最大的块
4、下次适应:从上一次分配位置继续查找
碎片问题:
1、内部碎片:分配块比实际需求大
2、外部碎片:空闲块分散,无法满足大请求
在hello中,printf内部可能调用malloc分配格式化缓冲区。
7.10本章小结
本章深入分析了hello程序的存储管理机制。通过/proc/$PID/maps查看了进程的虚拟地址空间布局,理解了逻辑地址、线性地址、虚拟地址和物理地址的转换关系。x86-64架构使用四级页表将48位虚拟地址转换为物理地址,TLB缓存加速了这一过程。CPU的三级缓存层次(L1、L2、L3)进一步减少了内存访问延迟。fork()系统调用使用写时复制技术高效创建子进程,execve()通过mmap()系统调用建立新的地址空间。缺页异常机制实现了按需分页和页面置换。动态存储分配管理通过malloc/free等函数支持程序的堆内存使用。这些存储管理机制共同保证了hello程序能够高效、安全地使用系统内存资源,实现了进程间的隔离和保护。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
1、设备的模型化:文件
Linux遵循"一切皆文件"的设计哲学,所有设备都被抽象为文件:
1)设备文件类型:
字符设备(c):以字符为单位进行I/O,如终端、键盘(实验统计:159个)
块设备(b):以数据块为单位进行I/O,如磁盘(实验统计:13个)
网络设备:通过套接字接口访问
2)设备文件位置:位于/dev目录下
/dev/pts/0 # 伪终端设备(当前终端)
/dev/tty # 控制终端
/dev/null # 空设备
/dev/zero # 零设备
3)设备号:主设备号(驱动类型)+ 次设备号(具体设备)
当前终端:/dev/pts/0,字符设备
标准输入输出错误都指向同一终端设备
2、设备管理:Unix IO接口
Linux通过统一的Unix IO接口管理所有设备:
相同的系统调用:open(), read(), write(), close()
相同的文件描述符机制
相同的缓冲和错误处理
8.2 简述Unix IO接口及其函数
1、基本IO函数
1)打开/关闭文件:
int open(const char *pathname, int flags, mode_t mode);
int close(int fd);
实验显示:打开文件返回fd=3(0,1,2已分配给标准流)
2)读写操作:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
实验显示:hello程序通过write(1, ...)输出,通过read(0, ...)读取输入
3)文件定位:
off_t lseek(int fd, off_t offset, int whence);
2、标准IO流
实验验证的标准文件描述符:
stdin:文件描述符0,指向/dev/pts/0
stdout:文件描述符1,指向/dev/pts/0
stderr:文件描述符2,指向/dev/pts/0
3、文件描述符表
每个进程维护一个文件描述符表:
前三个固定为stdin、stdout、stderr
新打开文件分配最小可用fd
fork()时子进程继承父进程的fd表
execve()时保持打开的fd(除非设置FD_CLOEXEC)
8.3 printf的实现分析
1、从vsprintf到write系统调用
printf的实现流程:
1)格式化处理:
int printf(const char *format, ...) {
va_list args;
va_start(args, format);
char buffer[BUFSIZ];
int len = vsprintf(buffer, format, args);
va_end(args);
return write(1, buffer, len);
}
2)系统调用转换:
printf() → vsprintf()生成格式化字符串
vsprintf() → write()系统调用输出
3)实验验证:
strace -e trace=write ./test_printf
# 输出:write(1, "测试printf: hello 123 45.600000\n", 34) = 34
2、write系统调用到显示输出
1)陷入内核:
write()触发syscall或int 0x80指令
CPU切换到内核态,保存用户态上下文
2)内核处理:
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count) {
struct file *file = fget(fd);
// 检查权限,复制用户空间数据到内核空间
// 调用文件操作表的write方法
return file->f_op->write(file, buf, count, &file->f_pos);
}
3)字符显示驱动:
终端设备:调用tty驱动
对于图形终端:转换为图形显示
ASCII字符 → 字模库 → 像素信息
写入显示VRAM(视频内存)
显示芯片逐行读取VRAM,通过信号线输出RGB分量
3、缓冲机制
实验验证的三种缓冲模式:
1)无缓冲(_IONBF):
setbuf(stdout, NULL);
printf("无缓冲输出1 ");
sleep(2);
printf("无缓冲输出2\n");
输出:立即显示"无缓冲输出1 ",2秒后显示"无缓冲输出2"
2)行缓冲(_IOLBF,终端默认):
setvbuf(stdout, NULL, _IOLBF, 0);
printf("行缓冲输出1 ");
sleep(2);
printf("行缓冲输出2\n");
输出:等待2秒后同时显示"行缓冲输出1 行缓冲输出2"
3)全缓冲(_IOFBF,文件默认):
setvbuf(stdout, NULL, _IOFBF, 1024);
printf("全缓冲输出");
sleep(2);
fflush(stdout);
输出:2秒后显示"全缓冲输出"
8.4 getchar的实现分析
1、键盘中断处理
1)硬件中断:
按键触发键盘控制器产生中断IRQ1
CPU响应中断,跳转到键盘中断处理程序
2)扫描码转换:
// 键盘中断处理程序简化的伪代码
void keyboard_interrupt_handler() {
unsigned char scancode = inb(0x60); // 读取扫描码
char ascii = scancode_to_ascii(scancode); // 转换为ASCII
add_to_keyboard_buffer(ascii); // 存入系统键盘缓冲区
}
3)系统缓冲区:
环形缓冲区存储按键ASCII码
支持行编辑(退格、删除等)
回车键触发行提交
2、getchar函数实现
1)库函数层:
int getchar(void) {
unsigned char c;
if (read(0, &c, 1) == 1) // 从stdin读取1字节
return c;
return EOF;
}
2)系统调用层:
// read系统调用处理终端输入
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count) {
if (fd == 0) { // 标准输入
// 等待键盘缓冲区有数据
// 从tty线路规程获取字符
// 支持规范模式(行缓冲)和非规范模式(字符缓冲)
}
}
3)实验验证:
./test_getchar2
# 输出:请输入一个字符(然后按回车):
# 输入:a
# 输出:你输入的字符是: 'a' (ASCII: 97)
3、终端I/O模式
1)规范模式(默认):
行缓冲,回车后read才返回
支持行编辑(退格、删除)
hello程序使用的模式
2)非规范模式:
字符缓冲,输入立即可用
用于实时交互程序
3)原始模式:
完全无处理,包括Ctrl+C等特殊字符
用于终端仿真器等
4、hello程序的输入处理
strace跟踪显示:
read(0, "\n", 1024) = 1 # 读取回车键
hello程序在循环结束后调用getchar()等待输入
用户按回车键,read返回换行符'\n'
程序继续执行,最终退出
8.5本章小结
本章深入分析了hello程序的I/O管理机制。Linux通过"一切皆文件"的哲学,将设备抽象为文件,提供统一的Unix I/O接口。实验验证了标准文件描述符0、1、2分别对应stdin、stdout、stderr,都指向终端设备/dev/pts/0。printf函数通过格式化字符串生成和write系统调用实现输出,涉及用户态到内核态的切换、终端驱动调用和显示硬件操作。getchar函数通过read系统调用从键盘缓冲区获取输入,涉及键盘中断处理、扫描码转换和终端I/O模式管理。缓冲机制(无缓冲、行缓冲、全缓冲)在不同场景下平衡了I/O效率和实时性。这些I/O管理机制使得hello程序能够与用户进行交互,在终端上显示输出并接收键盘输入,完成了程序与外部世界的通信。
结论
1、hello所经历的过程总结
1)预处理阶段:hello.c经过预处理器处理,展开所有头文件,删除注释,添加行号标记,生成hello.i文件。原始的592字节源代码扩展为64KB的预处理文件。
2)编译阶段:编译器将hello.i转换为x86-64汇编代码hello.s,包含83行汇编指令。编译器进行了语法分析、语义分析、中间代码生成和优化,将高级语言结构映射为底层机器指令。
3)汇编阶段:汇编器将hello.s转换为可重定位目标文件hello.o,包含157字节的机器代码。生成ELF格式文件,包含.text、.rodata等节区,以及8个需要重定位的符号引用。
4)链接阶段:链接器将hello.o与C标准库链接,生成可执行文件hello。完成符号解析和重定位,为程序分配虚拟地址(入口点0x4010f0,main函数0x4011d6),建立PLT/GOT动态链接机制。
5)加载阶段:shell通过fork()创建子进程,execve()系统调用加载hello。操作系统建立虚拟地址空间映射,代码段映射到0x401000,数据段映射到0x403e10,动态链接器解析库函数地址。
6)执行阶段:进程从_start开始执行,初始化运行环境后调用main函数。程序检查命令行参数,循环10次输出格式化字符串,每次间隔指定秒数,最后等待用户输入回车键。
7)存储管理:进程使用四级页表进行虚拟地址到物理地址转换,TLB缓存加速地址转换。采用写时复制技术实现fork高效内存共享,按需分页机制管理物理内存。
8)I/O管理:通过Unix I/O接口与终端交互,printf调用write系统调用输出到标准输出,getchar调用read系统调用从标准输入读取。缓冲机制平衡I/O效率与实时性。
9)进程管理:进程可响应各种信号(SIGINT、SIGTSTP、SIGCONT等),在不同状态间转换(运行、睡眠、停止)。调度器通过时间片轮转实现并发执行。
10)终止阶段:程序返回0,通过exit_group系统调用终止。操作系统回收进程所有资源(内存、文件描述符、PCB等),进程状态回归为零。
2、深切感悟与创新理念
对计算机系统设计与实现的深切感悟
1)层次化设计的精妙:计算机系统通过清晰的层次结构(应用层、运行时系统、操作系统、硬件)实现了复杂功能的模块化。每一层为上层提供抽象接口,隐藏下层实现细节,这种设计使得系统既强大又灵活。
2)抽象的力量:从"一切皆文件"的设备抽象,到虚拟内存的地址空间抽象,再到进程的CPU抽象,这些抽象机制极大地简化了编程模型,提高了系统安全性和可移植性。
3)性能与功能的平衡:系统设计处处体现了权衡思想。如TLB加速地址转换但容量有限,缓存提高内存访问速度但需要一致性维护,动态链接减少内存占用但增加启动开销。
4)透明性优化:许多优化对程序员完全透明,如写时复制、按需分页、延迟绑定等。这些机制在不改变接口的前提下显著提升系统性能。
5)错误的优雅处理:从硬件异常到软件信号,系统提供了完整的错误处理链。缺页异常、除零错误等都能被捕获并妥善处理,保证系统稳定性。
创新理念与设计思考
1)智能预加载机制:基于程序行为分析,可在链接阶段预测常用库函数,提前完成动态链接的符号解析,减少运行时延迟绑定的开销。
2)自适应页表层级:根据进程实际使用的地址空间范围,动态调整页表层级。对于小型程序可使用三级页表,大型程序使用四级页表,减少页表内存占用。
3)上下文感知的缓存策略:根据进程的IO模式(顺序/随机、读/写比例)动态调整页面置换算法和文件系统缓存策略,提高缓存命中率。
4)轻量级进程快照:结合容器技术,实现进程状态的快速保存和恢复。利用写时复制和内存去重技术,使多个相似进程共享大部分内存页。
5)可观测性增强:在ELF格式中增加可选的执行轨迹记录段,记录程序执行过程中的关键事件(函数调用、系统调用、缺页异常等),便于调试和性能分析。
6)能效优化的调度:考虑CPU能效特性的进程调度,将计算密集型任务分配给高性能核心,IO密集型任务分配给高能效核心,在保证性能的同时降低能耗。
7)安全增强的地址空间:为敏感数据(密码、密钥等)分配专用的地址空间区域,硬件支持该区域的访问控制和加密,防止内存泄露攻击。
通过本次大作业,我不仅掌握了计算机系统的基本原理,更深刻理解了系统设计中的权衡艺术。计算机系统是一个充满智慧和创造力的领域,每一处设计都凝聚着前人的思考和智慧。未来的系统设计需要在性能、安全、能效和易用性之间找到更好的平衡点,这需要我们不断探索和创新。
附件
列出所有的中间产物的文件名,并予以说明起作用。
| 文件名 | 生成命令 | 文件作用 |
| hello.c | 手工编写 | 原始C语言源代码,包含程序逻辑 |
| hello.i | gcc -E hello.c -o hello.i | 预处理后的源代码,包含所有展开的头文件 |
| hello.s | gcc -S hello.c -o hello.s | 汇编语言文件,包含x86-64汇编指令 |
| hello.o | gcc -c hello.c -o hello.o | 可重定位目标文件,包含机器指令但未解析外部引用 |
| hello | gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC hello.c -o hello | 可执行目标文件,包含完全解析的机器代码 |
| hello.dis | objdump -d hello > hello.dis | hello可执行文件的反汇编代码,便于分析 |
参考文献
[1] Randal E. Bryant, David R. O'Hallaron. *Computer Systems: A Programmer's Perspective, 3rd Edition*. Pearson, 2016.
[2] Intel Corporation. *Intel® 64 and IA-32 Architectures Software Developer Manuals*. https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
[3] GNU Compiler Collection. *GCC Online Documentation*. https://gcc.gnu.org/onlinedocs/
[4] Linux Programmer's Manual. *Linux man-pages project*. https://www.kernel.org/doc/man-pages/
[5] Tool Interface Standard (TIS). *Executable and Linking Format (ELF) Specification, Version 1.2*. https://refspecs.linuxfoundation.org/elf/elf.pdf
[6] GNU Binutils. *Binary Utilities Documentation*. https://sourceware.org/binutils/docs/
[7] Linux Kernel Organization. *Linux Kernel Source Tree*. https://www.kernel.org/
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/andy2106/article/details/156492668



