程序人生-Hello’s P2P

摘 要

本论文以简单的"Hello"程序为例,系统性地追踪和分析了一个C语言程序在Linux系统下从源代码到进程执行的全生命周期过程。通过分阶段实验,详细研究了程序预处理、编译、汇编、链接、加载、执行和终止的完整流程,深入揭示了计算机系统各层次间的协作机制。

主要内容包括:分析预处理阶段头文件展开和宏处理机制;研究编译器将C代码转换为汇编代码的优化策略;剖析汇编器生成可重定位目标文件的ELF格式结构;探讨链接器解析符号引用和地址重定位的工作原理;跟踪操作系统加载可执行文件创建进程的过程;观察进程执行中的信号处理和存储管理机制;分析程序I/O操作的底层实现。

研究方法采用自底向上的系统分析方法,结合GCC工具链、readelf、objdump、gdb等系统工具,对每个转换阶段生成的中间文件进行结构分析和代码对比。通过实际测试验证程序对键盘信号(Ctrl-C、Ctrl-Z等)的响应机制,并使用进程管理命令监控程序执行状态。

研究成果包括完整记录了程序转换各阶段的中间文件,验证了计算机系统抽象层次的有效性,展示了编译优化、动态链接、虚拟内存、进程隔离等核心系统原理的实际表现。本论文对理解程序在计算机系统中的完整执行过程具有理论意义,对系统编程、性能优化和安全分析具有实际参考价值。

**关键词:**计算机系统;程序生命周期;编译链接;进程管理;存储管理;ELF格式

自媒体发表截图

目 录

[第1章 概述. - 5 -](#第1章 概述. - 5 -)

[1.1 Hello简介. - 5 -](#1.1 Hello简介. - 5 -)

[1.2 环境与工具. - 5 -](#1.2 环境与工具. - 5 -)

[1.3 中间结果. - 5 -](#1.3 中间结果. - 5 -)

[1.4 本章小结. - 6 -](#1.4 本章小结. - 6 -)

[第2章 预处理. - 7 -](#第2章 预处理. - 7 -)

[2.1 预处理的概念与作用. - 7 -](#2.1 预处理的概念与作用. - 7 -)

[2.2在Ubuntu下预处理的命令. - 7 -](#2.2在Ubuntu下预处理的命令. - 7 -)

[2.3 Hello的预处理结果解析. - 8 -](#2.3 Hello的预处理结果解析. - 8 -)

[2.4 本章小结. - 10 -](#2.4 本章小结. - 10 -)

[第3章 编译. - 11 -](#第3章 编译. - 11 -)

[3.1 编译的概念与作用. - 11 -](#3.1 编译的概念与作用. - 11 -)

[3.2 在Ubuntu下编译的命令. - 11 -](#3.2 在Ubuntu下编译的命令. - 11 -)

[3.3 Hello的编译结果解析. - 14 -](#3.3 Hello的编译结果解析. - 14 -)

[3.4 本章小结. - 17 -](#3.4 本章小结. - 17 -)

[第4章 汇编. - 18 -](#第4章 汇编. - 18 -)

[4.1 汇编的概念与作用. - 18 -](#4.1 汇编的概念与作用. - 18 -)

[4.2 在Ubuntu下汇编的命令. - 18 -](#4.2 在Ubuntu下汇编的命令. - 18 -)

[4.3 可重定位目标elf格式. - 18 -](#4.3 可重定位目标elf格式. - 18 -)

[4.4 Hello.o的结果解析. - 21 -](#4.4 Hello.o的结果解析. - 21 -)

[4.5 本章小结. - 24 -](#4.5 本章小结. - 24 -)

[第5章 链接. - 25 -](#第5章 链接. - 25 -)

[5.1 链接的概念与作用. - 25 -](#5.1 链接的概念与作用. - 25 -)

[5.2 在Ubuntu下链接的命令. - 25 -](#5.2 在Ubuntu下链接的命令. - 25 -)

[5.3 可执行目标文件hello的格式. - 26 -](#5.3 可执行目标文件hello的格式. - 26 -)

[5.4 hello的虚拟地址空间. - 30 -](#5.4 hello的虚拟地址空间. - 30 -)

[5.5 链接的重定位过程分析. - 31 -](#5.5 链接的重定位过程分析. - 31 -)

[5.6 hello的执行流程. - 33 -](#5.6 hello的执行流程. - 33 -)

[5.7 Hello的动态链接分析. - 38 -](#5.7 Hello的动态链接分析. - 38 -)

[5.8 本章小结. - 38 -](#5.8 本章小结. - 38 -)

[第6章 hello进程管理. - 40 -](#第6章 hello进程管理. - 40 -)

[6.1 进程的概念与作用. - 40 -](#6.1 进程的概念与作用. - 40 -)

[6.2 简述壳Shell-bash的作用与处理流程. - 40 -](#6.2 简述壳Shell-bash的作用与处理流程. - 40 -)

[6.3 Hello的fork进程创建过程. - 40 -](#6.3 Hello的fork进程创建过程. - 40 -)

[6.4 Hello的execve过程. - 41 -](#6.4 Hello的execve过程. - 41 -)

[6.5 Hello的进程执行. - 41 -](#6.5 Hello的进程执行. - 41 -)

[6.6 hello的异常与信号处理. - 42 -](#6.6 hello的异常与信号处理. - 42 -)

[6.7本章小结. - 45 -](#6.7本章小结. - 45 -)

[第7章 hello的存储管理. - 46 -](#第7章 hello的存储管理. - 46 -)

[7.1 hello的存储器地址空间. - 46 -](#7.1 hello的存储器地址空间. - 46 -)

[7.2 Intel逻辑地址到线性地址的变换-段式管理. - 46 -](#7.2 Intel逻辑地址到线性地址的变换-段式管理. - 46 -)

[7.3 Hello的线性地址到物理地址的变换-页式管理. - 46 -](#7.3 Hello的线性地址到物理地址的变换-页式管理. - 46 -)

[7.4 TLB与四级页表支持下的VA到PA的变换. - 47 -](#7.4 TLB与四级页表支持下的VA到PA的变换. - 47 -)

[7.5 三级Cache支持下的物理内存访问. - 47 -](#7.5 三级Cache支持下的物理内存访问. - 47 -)

[7.6 hello进程fork时的内存映射. - 48 -](#7.6 hello进程fork时的内存映射. - 48 -)

[7.7 hello进程execve时的内存映射. - 48 -](#7.7 hello进程execve时的内存映射. - 48 -)

[7.8 缺页故障与缺页中断处理. - 48 -](#7.8 缺页故障与缺页中断处理. - 48 -)

[7.9动态存储分配管理. - 49 -](#7.9动态存储分配管理. - 49 -)

[7.10本章小结. - 49 -](#7.10本章小结. - 49 -)

[第8章 hello的IO管理. - 50 -](#第8章 hello的IO管理. - 50 -)

[8.1 Linux的IO设备管理方法. - 50 -](#8.1 Linux的IO设备管理方法. - 50 -)

[8.2 简述Unix IO接口及其函数. - 50 -](#8.2 简述Unix IO接口及其函数. - 50 -)

[8.3 printf的实现分析. - 51 -](#8.3 printf的实现分析. - 51 -)

[8.4 getchar的实现分析. - 52 -](#8.4 getchar的实现分析. - 52 -)

[8.5本章小结. - 52 -](#8.5本章小结. - 52 -)

[结论. - 53 -](#结论. - 53 -)

[附件. - 55 -](#附件. - 55 -)

[参考文献. - 56 -](#参考文献. - 56 -)

第1章 概述

1.1 Hello简介

P2P过程(从Program到Process):

1.编写程序:创建hello.c源代码文件

2.预处理:gcc -E 将#include和宏展开,生成hello.i

3.编译:gcc -S 将C代码转换为汇编代码,生成hello.s

4.汇编:gcc -c 将汇编代码转换为机器指令,生成hello.o

5.链接:gcc 将hello.o与库文件链接,生成可执行文件hello

6.加载运行:shell调用execve加载hello到内存创建进程

020过程(从Zero到Zero):

1.Zero:程序存储在磁盘上,没有执行

2.运行:shell创建子进程,execve加载程序,开始执行

3.执行中:进程在CPU上运行,调用系统函数

4.终止:main函数返回或调用exit,进程终止

5.Zero:进程终止,资源被回收,返回初始状态

1.2 环境与工具

硬件环境:x86-64 架构 PC

操作系统:Ubuntu 20.04 LTS(64 位)

编译器:gcc 9.x

调试工具:gdb、objdump、readelf

编辑器:VS Code / Vim

Shell:bash

1.3 中间结果

hello_linux.c - C语言源代码文件

hello.i - 预处理后文件(展开头文件)

hello.s - 汇编代码文件

hello.o - 可重定位目标文件(未链接)

hello - 最终可执行文件

hello_elf.txt - hello的ELF格式分析

hello_o_elf.txt - hello.o的ELF格式分析

hello_disasm.txt - hello反汇编代码

hello_o_disasm.txt - hello.o反汇编代码

hello_verbose - gcc详细编译日志

1.4 本章小结

本章介绍了Hello程序的P2P和020过程,说明了实验环境和工具,列出了实验中将生成的中间文件。

第2章 预处理

2.1 预处理的概念与作用

预处理是C语言编译过程的第一阶段,发生在源代码被真正编译之前。其主要作用是对源代码进行文本级别的处理,包括以下几个方面:

1.宏定义展开:将程序中所有通过#define定义的宏进行替换

2.文件包含:将#include指令指定的头文件内容插入到指令位置

3.条件编译:根据#if、#ifdef、#ifndef等条件编译指令决定是否编译特定代码段

4.注释删除:移除所有注释(包括单行注释//和多行注释/* */)

5.添加行号信息:为调试和错误报告添加行号和文件名信息

6.处理特殊指令:如#pragma、#error等特殊预处理指令

预处理不会进行语法检查,也不改变程序的语义,它只是为后续的编译阶段准备一个"纯净"的C代码文件。

2.2在Ubuntu下预处理的命令

图2-1 预处理命令执行以及生成的文件

执行预处理命令

gcc -E hello_linux.c -o hello.i

2.3 Hello的预处理结果解析

2.3.1 行号标记分析

预处理文件开头包含大量行号标记,这些标记的格式为:

行号 "文件名" 标志位

2.3.2 头文件包含展开

预处理文件展示了头文件包含的完整展开过程:

1.stdio.h的展开:

源文件中的#include <stdio.h>被替换为stdio.h的全部内容

从输出可以看到,stdio.h又包含了多个其他头文件:

/usr/include/x86_64-linux-gnu/bits/libc-header-start.h

/usr/include/features.h

/usr/include/x86_64-linux-gnu/sys/cdefs.h

/usr/include/x86_64-linux-gnu/bits/wordsize.h

/usr/include/x86_64-linux-gnu/bits/long-double.h

/usr/include/x86_64-linux-gnu/gnu/stubs.h

/usr/include/x86_64-linux-gnu/gnu/stubs-64.h

2.unistd.h的展开:

源文件中的#include <unistd.h>被替换为unistd.h的相关内容

从文件末尾可以看到unistd.h包含了函数声明如crypt和getentropy

2.3.3 注释处理

对比源文件和预处理文件可以发现:

源文件开头的注释// hello_linux.c - 金朝 (2024113129) 的Linux版本在预处理文件中完全消失

预处理阶段删除了所有注释,不保留任何痕迹

2.3.4 用户代码保留

预处理文件最后部分(约30行)显示了用户编写的main函数完整内容:

int main() {

int i;

char student_id[] = "2024113129";

char student_name[] = "金朝";

printf("=== 学号:2024113129 姓名:金朝 ===\n");

printf("=== 手机号:13589208935 ===\n");

for (i = 0; i < 5; i++) {

printf("第%d次:Hello %s %s\n", i+1, student_id, student_name);

sleep(2);

}

printf("程序执行完毕,按回车键退出...\n");

getchar();

return 0;

}

2.3.5 关键预处理效果总结

|-------|---------|---------------------|
| 预处理功能 | 实际效果 | 在hello.i中的体现 |
| 文件包含 | 头文件内容插入 | stdio.h和unistd.h被展开 |
| 注释删除 | 移除所有注释 | 源文件注释消失 |
| 行号标记 | 添加调试信息 | 每行前的# 行号 "文件名"标记 |
| 条件编译 | 处理条件指令 | 头文件中的条件编译被处理 |
| 宏展开 | 替换宏定义 | 头文件中的宏被展开 |

2.4 本章小结

本章完成了C源程序的预处理分析。通过执行gcc -E hello_linux.c -o hello.i命令,成功生成了预处理文件hello.i。该文件大小为38KB,共1985行,相比原始源文件的约400字节显著增大。

预处理阶段主要完成了头文件展开、注释删除和行号信息添加等工作。源文件中的两个#include指令被完全展开,包含了stdio.h和unistd.h的所有内容,形成复杂的头文件包含链。注释被彻底删除,用户的main函数代码完整保留在文件末尾。

通过分析预处理结果,验证了预处理器的基本功能:宏展开、文件包含、条件编译处理等。预处理文件虽然庞大,但为后续的编译阶段提供了"纯净"的C代码,所有必要的声明和定义都已包含,为语法分析和代码生成做好了准备。

第3章 编译

3.1 编译的概念与作用

编译是将预处理后的C代码(.i文件)转换为汇编代码(.s文件)的过程。这是编译器的核心阶段,主要包括以下步骤:

1.词法分析:将源代码分解为词法单元(tokens)

2.语法分析:根据语法规则构建抽象语法树(AST)

3.语义分析:检查类型、作用域等语义规则

4.中间代码生成:生成与机器无关的中间表示

5.代码优化:对中间代码进行优化

6.目标代码生成:生成目标机器的汇编代码

编译阶段的作用是将高级语言转换为低级语言,为后续的汇编阶段做准备。

3.2 在Ubuntu下编译的命令

图3-1 编译命令执行以及生成的文件、汇编代码、main函数部分

执行编译命令

gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析

3.3.1 汇编文件整体结构分析

.file "hello_linux.c" # 源文件名

.text # 代码段开始

.section .rodata # 只读数据段

文件头信息说明

.file "hello_linux.c":标识源文件名为hello_linux.c

.text:代码段(text segment),存放可执行指令

.section .rodata:只读数据段,存放字符串常量等不可修改数据

3.3.2 字符串常量定义分析

.LC0:

.string"===\345\255\246\345\217\267\357\274\23220\ 24113129 \345\247\223\345\220\215\357\274\232\351\207\215\346\234\210 ==="

.LC1:

.string"===\346\211\213\346\234\272\345\217\267\357\274\23213589208935 ==="

.LC2:

.string"\347\254\254%d\346\254\241\357\274\232Hello %s %s\n"

.LC3:

.string"\347\250\213\345\272\217\346\211\247\350\241\214\345\256\214\346\257\225\357\274\214\346\214\211\345\233\236\350\275\246\351\224\256\351\200\200\345\207\272..."

字符串常量解析

1..LC0:对应C代码"=== 学号:2024113129 姓名:金朝 ==="

中文字符使用UTF-8八进制编码:\345\255\246="学",\345\217\267="号"

.align 8:8字节对齐,提高访问效率

2..LC1:对应"=== 手机号:13589208935 ==="

3..LC2:对应格式字符串"第%d次:Hello %s %s\n"

包含格式说明符%d和%s

4..LC3:对应"程序执行完毕,按回车键退出..."

3.3.3 main 函数分析

1. 函数入口与栈帧建立

main:

endbr64 # 安全特性:控制流保护

pushq %rbp # 保存旧的栈基址

movq %rsp, %rbp # 设置新的栈基址

subq $32, %rsp # 分配32字节栈空间

2. 局部变量初始化

int i:栈位置-32(%rbp)

char student_id[]:栈位置-19(%rbp)到-9(%rbp)

char student_name[]:栈位置-26(%rbp)到-20(%rbp)

3. 前两个输出语句(编译器优化)

leaq .LC0(%rip), %rdi

call puts@PLT # 优化:printf → puts

编译器将简单的printf("字符串\n")优化为puts("字符串")。

4. for 循环实现

循环初始化:

movl $0, -32(%rbp) # i = 0

jmp .L2 # 跳转到条件判断

循环体(.L3标签):

.L3:

准备printf参数

movl -32(%rbp), %eax # 加载i

leal 1(%rax), %esi # esi = i+1 (优化:使用lea)

leaq -26(%rbp), %rdx # rdx = &student_name

leaq -19(%rbp), %rax # rax = &student_id

movq %rdx, %rcx # rcx = &student_name

movq %rax, %rdx # rdx = &student_id

leaq .LC2(%rip), %rdi # rdi = 格式字符串

call printf@PLT # 调用printf

sleep调用

movl $2, %edi

call sleep@PLT

i++

addl $1, -32(%rbp)

条件判断(.L2标签):

.L2:

cmpl $4, -32(%rbp) # 比较i和4

jle .L3 # 如果i <= 4,继续循环

C语言的i < 5在汇编中实现为i <= 4。

5. 结束部分

leaq .LC3(%rip), %rdi

call puts@PLT # 输出结束信息

call getchar@PLT # 等待输入

6. 函数返回

movl $0, %eax # 返回值0

leave # 恢复栈指针

ret # 返回

3.3.4 编译器优化特性

1.字符串优化:将printf("字符串\n")优化为puts("字符串")

2.算术优化:使用leal指令进行i+1计算,比单独add+mov更高效

3.寄存器优化:充分利用寄存器传递参数

4.安全增强:包含endbr64指令和栈保护机制

3.3.5 调用约定

x86-64 System V调用约定:

前4个参数:rdi, rsi, rdx, rcx

返回值:rax

栈对齐:16字节对齐

3.4 本章小结

本章完成了从预处理文件到汇编代码的编译分析。通过执行gcc -S hello.i -o hello.s命令,生成了1.8KB的汇编文件hello.s。

编译阶段将高级C语言转换为低级汇编语言,展现了代码的结构化转换过程。分析发现编译器进行了多项优化:将简单的printf("字符串\n")调用优化为更高效的puts("字符串");使用leal指令进行算术运算优化;合理分配寄存器传递函数参数。

汇编代码清晰地展示了main函数的实现结构:栈帧建立、局部变量初始化、循环控制逻辑、函数调用约定等。中文字符串以UTF-8编码形式存储在.rodata段,循环结构被转换为标签和条件跳转指令。

通过本章分析,理解了编译器如何将高级语言结构映射到机器相关的汇编指令,以及现代编译器的优化策略,为后续的汇编阶段奠定了基础。

第4章 汇编

4.1 汇编的概念与作用

汇编是将汇编代码(.s文件)转换为机器指令(.o文件)的过程。汇编器处理汇编指令、符号引用和重定位信息,生成可重定位目标文件。主要作用包括:

1.指令转换:将汇编助记符转换为机器码

2.符号解析:处理标签和符号引用

3.生成目标文件:创建ELF格式的可重定位目标文件

4.重定位信息:记录需要链接时处理的符号引用

4.2 在Ubuntu下汇编的命令

图4-1 汇编命令执行及结果

执行汇编命令

gcc -c hello.s -o hello.o

4.3 可重定位目标elf格式

4.3.1 ELF 头信息

文件类型:可重定位文件(REL)

架构:x86-64,小端序

节头表位置:偏移1456字节

节数量:14个节

4.3.2 主要节区分析

关键节区:

1..text节(代码段)

偏移:0x40

大小:0xca (202字节)

标志:AX(可执行、可分配)

包含main函数的机器指令

2..rodata节(只读数据段)

偏移:0x110

大小:0x93 (147字节)

标志:A(可分配)

存放字符串常量:"学号..."、"手机号..."等

3..data/.bss节

大小均为0,说明程序无全局/静态变量

4.重定位节

.rela.text:代码段重定位表(11项)

.rela.eh_frame:异常帧重定位表(1项)

4.3.3 符号表分析(17个符号)

图4-2 符号表信息

关键符号:

1.main:全局函数,位于.text节,大小202字节

2.未定义符号(UND):

puts、printf、sleep、getchar、__stack_chk_fail

需要在链接时解析

4.3.4 重定位项目分析

图4-3 重定位表信息

|------|----------------|----------------------|-----------|
| 偏移 | 类型 | 符号 | 说明 |
| 0x47 | R_X86_64_PC32 | .rodata - 4 | 第一个字符串地址 |
| 0x4c | R_X86_64_PLT32 | puts - 4 | 第一个puts调用 |
| 0x53 | R_X86_64_PC32 | .rodata + 0x2c | 第二个字符串地址 |
| 0x58 | R_X86_64_PLT32 | puts - 4 | 第二个puts调用 |
| 0x7c | R_X86_64_PC32 | .rodata + 0x4c | 格式字符串地址 |
| 0x86 | R_X86_64_PLT32 | printf - 4 | printf调用 |
| 0x90 | R_X86_64_PLT32 | sleep -- 4 | sleep调用 |
| 0xa1 | R_X86_64_PC32 | .rodata + 0x64 | 结束字符串地址 |
| 0xa6 | R_X86_64_PLT32 | puts - 4 | 结束puts调用 |
| 0xab | R_X86_64_PLT32 | getchar - 4 | getchar调用 |
| 0xc4 | R_X86_64_PLT32 | __stack_chk_fail - 4 | 栈保护失败处理 |

重定位类型说明:

1.R_X86_64_PC32(PC相对32位)

用于.rodata中的字符串地址引用

计算方式:目标地址 - (当前指令地址 + 4)

2.R_X86_64_PLT32(PLT条目32位)

用于函数调用,通过过程链接表(PLT)

实现延迟绑定,提高动态链接效率

重定位偏移对应关系:

0x47:对应反汇编中第一个lea 0x0(%rip),%rdi的地址部分

0x4c:对应第一个callq指令

0x86:对应printf调用

以此类推...

4.3.5 总结

1.hello.o是标准的可重定位ELF文件

2.包含202字节代码和147字节只读数据

3.有11个需要重定位的符号引用

4.所有外部函数调用都通过PLT重定位

5.字符串地址通过PC相对重定位解决

4.4 Hello.o的结果解析

图4-4 objdump反汇编

4.4.1 objdump 反汇编分析

0000000000000000 <main>:

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 # 分配栈空间

栈保护

c: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax

15: 48 89 45 f8 mov %rax,-0x8(%rbp)

student_id = "2024113129"

1b: 48 b8 32 30 32 34 31 movabs $0x3133313134323032,%rax

25: 48 89 45 ed mov %rax,-0x13(%rbp)

student_name = "金朝"

33: c7 45 e6 e9 87 91 e6 movl $0xe69187e9,-0x1a(%rbp)

puts调用(需重定位)

44: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi

4b: e8 00 00 00 00 callq 50 <main+0x50>

for循环

5c: c7 45 e0 00 00 00 00 movl $0x0,-0x20(%rbp) # i=0

63: eb 33 jmp 98 <main+0x98> # 跳转

循环体

65: 8b 45 e0 mov -0x20(%rbp),%eax # 加载i

68: 8d 70 01 lea 0x1(%rax),%esi # i+1

printf调用(需重定位)

79: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi

85: e8 00 00 00 00 callq 8a <main+0x8a>

条件判断

98: 83 7d e0 04 cmpl $0x4,-0x20(%rbp) # i<=4?

9c: 7e c7 jle 65 <main+0x65> # 继续循环

c8: c9 leaveq

c9: c3 retq

4.4.2 与hello.s对照分析

主要差异:

|-------|------------|---------------------|
| 对比项 | hello.s | hello.o |
| 标签 | .L2, .L3 | 0x65, 0x98(具体地址) |
| 外部调用 | puts@PLT | callq(地址全0,需重定位) |
| 字符串引用 | .LC0(%rip) | lea 0x0(%rip)(偏移为0) |

4.4.3 机器语言特点

1. 指令编码

简单指令:push %rbp → 55(单字节)

复杂指令:movabs → 多字节,包含立即数

相对跳转:jle 65 → 7e c7(c7=-57)

2. 重定位需求

函数调用:e8 00 00 00 00(callq)

字符串地址:lea 0x0(%rip)

特点:地址部分为0,需链接时填充

3. 操作数处理

立即数:直接编码在指令中

内存引用:通过偏移量寻址

相对地址:便于代码重定位

4.4.4 关键发现

1.未解析符号:所有外部函数调用地址为0

2.相对寻址:跳转使用相对偏移,不依赖绝对地址

3.栈布局固定:局部变量在栈中位置已确定

4.重定位信息完整:为链接器提供了修正地址所需信息

4.5 本章小结

本章完成了从汇编代码到可重定位目标文件的转换分析。通过执行`gcc -c hello.s -o hello.o`命令,生成了2.3KB的ELF可重定位文件。深入分析了hello.o的ELF格式,包括代码段(.text)202字节、只读数据段(.rodata)147字节以及11个重定位项。

汇编阶段将hello.s中的汇编指令转换为机器码,但所有外部符号引用(如puts、printf等函数调用和字符串地址)均未解析,使用0占位符表示。这些未解析的引用被记录在重定位表中,为链接阶段的地址绑定提供了必要信息。

通过对比hello.s和hello.o,观察到汇编器完成了标签到具体地址的转换、相对跳转偏移量的计算等工作。然而,机器语言中的函数调用和外部数据引用仍保持未绑定状态,这体现了可重定位目标文件的核心特点:代码已编译完成,但最终内存地址需要链接器确定。

本章工作为理解链接过程奠定了基础,明确了汇编器在编译流程中的定位和作用。

第5章 链接

5.1 链接的概念与作用

链接是将多个可重定位目标文件合并生成可执行文件的过程。主要作用包括:

1.符号解析:将每个符号引用与一个符号定义关联

2.重定位:确定每个符号的最终内存地址,修改所有对这些符号的引

3.合并节区:将相同类型的节合并为段

4.添加运行时信息:设置程序入口、添加程序头等

5.2 在Ubuntu下链接的命令

图5-1 使用gcc链接生成可执行文件以及手动链接命令及结果

1. 使用gcc链接

gcc hello.o -o hello

ls -lh hello

2. 手动使用ld链接器

首先找到必要的启动文件

find /usr -name "crt1.o" 2>/dev/null

执行手动链接

ld -o hello_manual \

-dynamic-linker /lib64/ld-linux-x86-64.so.2 \

/usr/lib/x86_64-linux-gnu/crt1.o \

/usr/lib/x86_64-linux-gnu/crti.o \

hello.o \

-lc \

/usr/lib/x86_64-linux-gnu/crtn.o

验证手动链接结果

ls -lh hello_manual

./hello_manual

5.3 可执行目标文件hello的格式

5.3.1 ELF 头信息分析

执行命令: readelf -h hello

图5-2 hello的ELF头信息

关键信息总结:

1.文件类型:DYN (共享目标文件),实际上是位置无关可执行文件(PIE)

2.架构:x86-64,小端序

3.入口地址:0x10e0(程序开始执行的位置)

4.程序头:13个(段信息)

5.节头:31个(节区信息)

5.3.2 程序头表(段信息)分析

执行命令: readelf -l hello

图5-3 程序头表信息

主要段信息表:

|---------|--------|--------|-------|-------|-----|--------|
| 段类型 | 文件偏移 | 虚拟地址 | 文件大小 | 内存大小 | 标志 | 对齐 |
| PHDR | 0x40 | 0x40 | 0x2d8 | 0x2d8 | R | 0x8 |
| INTERP | 0x318 | 0x318 | 0x1c | 0x1c | R | 0x1 |
| LOAD1 | 0x0 | 0x0 | 0x700 | 0x700 | R | 0x1000 |
| LOAD2 | 0x1000 | 0x1000 | 0x325 | 0x325 | R E | 0x1000 |
| LOAD3 | 0x2000 | 0x2000 | 0x1e8 | 0x1e8 | R | 0x1000 |
| LOAD4 | 0x2d98 | 0x3d98 | 0x278 | 0x280 | RW | 0x1000 |
| DYNAMIC | 0x2da8 | 0x3da8 | 0x1f0 | 0x1f0 | RW | 0x8 |

段与节的映射关系:

段02(LOAD1,只读):包含动态链接元数据(.dynsym, .dynstr, .rela等)

段03(LOAD2,可执行):包含代码段(.init, .plt, .text, .fini)

段04(LOAD3,只读):包含只读数据(.rodata, .eh_frame等)

段05(LOAD4,读写):包含数据段(.data, .bss, .got, .dynamic等)

5.3.3 关键节区地址和大小

执行命令: readelf -S hello | grep -E "\.text|\.rodata|\.data|\.bss"

图5-4 关键节区信息

输出分析:

16\] .text PROGBITS 00000000000010e0 000010e0 000000000000013f 0000000000000000 AX 0 0 16 \[18\] .rodata PROGBITS 0000000000002000 00002000 000000000000008c 0000000000000000 A 0 0 8 \[25\] .data PROGBITS 0000000000004000 00003000 0000000000000010 0000000000000000 WA 0 0 8 \[26\] .bss NOBITS 0000000000004010 00003010 0000000000000008 0000000000000000 WA 0 0 1 **节区信息表:** |---------|--------|--------|-------|----|--------------| | 节区 | 虚拟地址 | 文件偏移 | 大小 | 标志 | 作用 | | .text | 0x10e0 | 0x10e0 | 0x13f | AX | 代码段,包含main函数 | | .rodata | 0x2000 | 0x2000 | 0x8c | A | 只读数据,字符串常量 | | .data | 0x4000 | 0x3000 | 0x10 | WA | 已初始化数据 | | .bss | 0x4010 | 0x3010 | 0x8 | WA | 未初始化数据 | **5.3.4** **入口点分析** 执行命令: objdump -d hello --start-address=0x10e0 \| head -20 ![](https://i-blog.csdnimg.cn/direct/48f4bba44dff4e9dab6ef02959a0ff5c.png) 图5-5 程序入口点_start函数 **输出分析:** 00000000000010e0 \<_start\>: 10e0: f3 0f 1e fa endbr64 10e1: 31 ed xor %ebp,%ebp ... 1101: 48 8d 3d c1 00 00 00 lea 0xc1(%rip),%rdi # 11c9 \ 1108: ff 15 d2 2e 00 00 callq \*0x2ed2(%rip) # 3fe0 \<__libc_start_main@GLIBC_2.2.5\> **关键发现:** 1.入口点是_start函数,而非main函数 2._start调用__libc_start_main,参数中包含main函数地址(0x11c9) 3.main函数实际地址为0x11c9 **5.3.5** **文件基本信息** **执行命令:** ls -lh hello file hello ![](https://i-blog.csdnimg.cn/direct/8e5d1faacd9b4497b38c2ce136aad58b.png) 图5-6 可执行文件信息 **文件特性:** 大小:17KB 类型:共享对象(实际上是PIE可执行文件) 动态链接:使用/lib64/ld-linux-x86-64.so.2作为解释器 包含构建ID:用于版本识别 **5.3.6** **内存布局总结** **虚拟地址空间布局:** 0x0000000000000000 - 0x0000000000000700 : LOAD1 (只读动态链接数据) 包含符号表、字符串表、重定位表等元数据 0x0000000000001000 - 0x0000000000001325 : LOAD2 (代码段,可执行) 入口点:0x10e0 (_start函数) 包含:.init, .plt, .text, .fini main函数地址:0x11c9 0x0000000000002000 - 0x00000000000021e8 : LOAD3 (只读数据段) 包含:.rodata (字符串常量), 异常处理帧 0x0000000000003d98 - 0x0000000000004018 : LOAD4 (读写数据段) 包含:.data, .bss, .got, .dynamic等 **5.3.7** **关键特点总结** PIE可执行文件:类型为DYN,支持地址空间布局随机化 动态链接:需要动态链接器,包含完整的动态链接结构 入口点:_start函数(0x10e0),而非main函数 安全特性:包含栈保护、只读重定位等安全机制 段对齐:4KB对齐,提高内存访问效率 ### 5.4 hello的虚拟地址空间 **5.4.1** **使用gdb查看内存布局** gdb ./hello (gdb) start (gdb) info proc mappings ![](https://i-blog.csdnimg.cn/direct/7560dbb9d659441aa80af5fc4bb6d7ba.png) 图5-7 gdb查看进程内存映射 **5.4.2** **与5.3节对照分析** **地址映射对照表:** |----------------|----------------|---------| | ELF文件(5.3节) | 运行时内存(5.4节) | 对应关系 | | 入口地址0x10e0 | 0x5555555550e0 | 基址+偏移 | | .text节0x10e0 | 0x5555555550e0 | 代码段 | | .rodata节0x2000 | 0x555555556000 | 只读数据段 | | .data节0x4000 | 0x555555557000 | 读写数据段 | | LOAD2段0x1000 | 0x555555555000 | 代码段基址 | | LOAD3段0x2000 | 0x555555556000 | 只读数据段基址 | **关键发现:** 1.加载基址:0x555555554000(每次运行随机变化,PIE特性) 2.段映射:4个LOAD段正确映射到4个4KB内存页 3.权限保护: 代码段:r-xp(可执行,不可写) 数据段:rw-p(可读写,不可执行) 只读段:r--p(只读) **5.4.3** **地址计算验证** ![](https://img-home.csdnimg.cn/images/20230724024159.png) **验证main函数地址:** 图5-8 main函数的地址信息 (gdb) info address main **计算过程**: 文件地址:0x11c9(从5.3节objdump得知) 运行时地址:0x555555555000 + 0x11c9 = 0x5555555551c9 ✓ **5.4.4** **内存布局总结** 虚拟地址空间: 0x555555554000-0x555555555000 : 只读段(动态链接数据) 0x555555555000-0x555555556000 : 代码段(可执行) 0x555555556000-0x555555557000 : 只读数据段 0x555555557000-0x555555558000 : 读写数据段 0x7ffff7dc9000-0x7ffff7dcb000 : 堆 0x7ffffffde000-0x7ffffffff000 : 栈 **5.4.5** **结论** 1.PIE生效:程序加载基址随机,增强安全性 2.段映射正确:ELF文件中的段正确映射到内存 3.权限保护完善:W\^X原则(不可同时可写和可执行) 4.地址转换公式:虚拟地址 = 加载基址 + 文件偏移 ### 5.5 链接的重定位过程分析 **5.5.1** **反汇编对比分析** **执行命令:** objdump -d -r hello.o \> hello_o_disasm.txt objdump -d -r hello \> hello_disasm.txt **hello.o** **(链接前):** 44: lea 0x0(%rip),%rdi # 地址占位符 4b: callq 00 00 00 00 # 函数未解析 **hello** **(链接后):** 120d: lea 0xdf4(%rip),%rdi # 字符串地址已计算 1214: callq 1090 \ # 函数已绑定 **5.5.2** **重定位类型分析** **11** **个重定位项:** 1.4个R_X86_64_PC32:字符串地址引用 2.7个R_X86_64_PLT32:函数调用引用 **5.5.3** **重定位计算示例** 1. 字符串地址计算(PC32) 目标地址:0x2008(字符串位置) 指令地址:0x120d(lea指令) 计算:0x2008 - (0x120d + 7) = 0xdf4 结果:lea 0xdf4(%rip) 2. 函数调用计算(PLT32) 目标地址:0x1090(puts@plt) 指令地址:0x1214(call指令) 计算:0x1090 - (0x1214 + 4) = -0x188 编码:e8 77 fe ff ff(小端序) **5.5.4** **重定位完成汇总** |------|-------|-------------------------| | 重定位项 | 类型 | 完成结果 | | 0x47 | PC32 | lea 0xdf4(%rip) → 字符串1 | | 0x4c | PLT32 | callq 1090 → puts调用 | | 0x7c | PC32 | lea 0xe0f(%rip) → 格式字符串 | | 0x86 | PLT32 | callq 10b0 → printf调用 | | 0x90 | PLT32 | callq 10d0 → sleep调用 | | 0xab | PLT32 | callq 10c0 → getchar调用 | **5.5.5** **链接器工作** 完成的任务: 1.解析符号:找到外部函数定义 2.分配地址:确定各段内存位置 3.计算偏移:填充PC相对地址 4.建立PLT:设置动态链接机制 技术特点: 位置无关:使用PC相对寻址支持ASLR 延迟绑定:PLT/GOT实现动态链接优化 安全保护:栈保护等安全特性 **5.5.6** **结论** 链接器通过重定位将hello.o中的未解析符号绑定到具体地址,生成可执行的hello文件,支持动态链接和安全特性。 ### 5.6 hello的执行流程 ![](https://i-blog.csdnimg.cn/direct/51fca54d1a8241e986f91580755df186.png)![](https://i-blog.csdnimg.cn/direct/1044022f9b574f0abd9a84369a4f0333.png)![](https://i-blog.csdnimg.cn/direct/e6cadfc2210e4a42bc9f4bfe22b408ec.png)![](https://i-blog.csdnimg.cn/direct/d4eed0dad7704535b0018e7a51d192ee.png) 图5-9 程序执行流程调试 **5.6.1** **执行流程阶段分析** **阶段1:内核加载阶段** 内核读取hello的ELF文件头 创建进程,设置虚拟地址空间 根据程序头表映射各段到内存 设置动态链接器路径为/lib64/ld-linux-x86-64.so.2 **阶段2:动态链接器执行阶段** 动态链接器ld-2.31.so开始执行(地址:0x7ffff7fcf000) 加载依赖的共享库libc-2.31.so(地址:0x7ffff7dbf000) 解析动态符号,填充全局偏移表(GOT) 完成重定位后,跳转到程序入口点_start **阶段3:程序入口阶段** 程序从_start开始执行,地址:0x5555555550e0 反汇编代码执行顺序: 1.endbr64 - 控制流保护指令 2.xor ebp, ebp - 清除帧指针 3.mov r9, rdx - 保存动态链接器完成函数 4.pop rsi - 获取argc参数 5.mov rdx, rsp - 获取argv指针 6.栈16字节对齐操作 7.设置__libc_csu_fini地址到r8寄存器 8.设置__libc_csu_init地址到rcx寄存器 9.设置main函数地址到rdi寄存器 10.调用__libc_start_main@got.plt **阶段4:libc初始化阶段** __libc_start_main执行: 调用__libc_csu_init函数(地址:0x5555555552a0) __libc_csu_init调用_init函数(地址:0x555555555000) _init执行全局构造器初始化 初始化完成后,调用main函数 **阶段5:用户代码执行阶段** main函数开始执行,地址:0x5555555551c9 main函数执行顺序: 1.栈帧建立:保存rbp,设置新的栈帧 2.栈保护机制:从fs:0x28读取栈保护值 3.分配32字节栈空间给局部变量 4.初始化局部变量: student_id数组初始化为"2024113129" student_name数组初始化为"金朝" 5.第一次字符串输出: 加载字符串地址到rdi寄存器 调用puts@plt(地址:0x555555555090) 输出:"=== 学号:2024113129 姓名:金朝 ===" 6.第二次字符串输出: 调用puts@plt 输出:"=== 手机号:13589208935 ===" 7.循环执行5次: 设置循环变量i=0 跳转到循环条件判断 循环体执行: 准备printf参数:i+1, student_id地址, student_name地址 调用printf@plt(地址:0x5555555550b0) 调用sleep@plt(地址:0x5555555550d0),参数2 i自增 条件判断:如果i\<=4,继续循环 8.程序结束部分: 调用puts@plt输出结束信息 调用getchar@plt(地址:0x5555555550c0)等待输入 9.函数返回: 设置返回值eax=0 检查栈保护值是否被修改 恢复栈帧,返回 初始化完成后,调用main函数 **阶段6:程序清理和退出阶段** main函数返回值0传递给__libc_start_main __libc_start_main调用__libc_csu_fini(地址:0x555555555310) __libc_csu_fini调用_fini函数(地址:0x555555555318) _fini执行全局析构器清理 执行atexit注册的清理函数 调用exit系统调用终止进程 内核回收进程资源 **5.6.2** **函数调用地址表** |------|-------------------|----------------|-------------------|----------------| | 执行顺序 | 调用函数 | 调用地址 | 目标函数 | 目标地址 | | 1 | (内核) | - | _start | 0x5555555550e0 | | 2 | _start | 0x5555555550e0 | __libc_start_main | 0x555555557fe | | 3 | __libc_start_main | 0x555555557fe0 | __libc_csu_init | 0x5555555552a0 | | 4 | __libc_csu_init | 0x5555555552a0 | _init | 0x555555555000 | | 5 | __libc_start_main | 0x555555557fe0 | main | 0x5555555551c9 | | 6 | main | 0x5555555551c9 | puts@plt | 0x555555555090 | | 7 | main | 0x5555555551c9 | puts@plt | 0x555555555090 | | 8 | main | 0x5555555551c9 | printf@plt | 0x5555555550b0 | | 9 | main | 0x5555555551c9 | sleep@plt | 0x5555555550d0 | | 10 | main | 0x5555555551c9 | puts@plt | 0x555555555090 | | 11 | main | 0x5555555551c9 | getchar@plt | 0x5555555550c0 | | 12 | __libc_start_main | 0x555555557fe0 | libc_csu_fini | 0x555555555310 | | 13 | __libc_csu_fini | 0x555555555310 | fini | 0x555555555318 | | 14 | __libc_start_main | 0x555555557fe0 | exit | 系统调用 | **5.6.3 PLT** **调用机制** 每个PLT调用(如puts@plt)的执行过程: 第一次调用时: 1.跳转到GOT表中对应的项 2.GOT项指向动态链接器的解析例程 3.动态链接器查找puts在libc中的实际地址 4.将实际地址写回GOT表 5.跳转到libc的puts函数执行 后续调用时: 1.直接跳转到GOT表中已保存的地址 2.执行libc的puts函数 **5.6.4** **调试验证结果** 通过gdb调试验证了以下关键点: 1._start断点成功命中:0x5555555550e0 2.main断点成功命中:0x5555555551c9 3.程序内存映射正确:5个程序段完整加载 4.寄存器状态正常:参数寄存器正确设置 5.执行流程完整:从_start到main再到正常退出 6.输出结果正确:显示了预期的5次循环信息 **5.6.5** **执行流程总结** hello程序的执行展现了完整的Linux程序生命周期: 内核加载和内存映射 动态链接器解析共享库 libc运行环境初始化 用户代码执行和I/O操作 资源清理和进程退出 ### 5.7 Hello的动态链接分析 ![](https://i-blog.csdnimg.cn/direct/fcb9e4dd935a422b856cc45a5e7f2632.png) 图5-10 动态链接器内存映射 **5.7.1** **关键发现** 早期绑定:程序启动时动态链接器已解析函数地址 GOT表已填充:puts地址0x7ffff7e43450在首次调用前已存入GOT 无延迟绑定:PLT直接跳转至libc,跳过解析过程 **5.7.3 GOT** **表项映射** |---------|----------------|----------------| | 函数 | GOT地址 | 实际地址 | | puts | 0x555555557fb0 | 0x7ffff7e43450 | | printf | 0x555555557fc0 | libc地址 | | sleep | 0x555555557fd0 | libc地址 | | getchar | 0x555555557fc8 | libc地址 | **5.7.4** **结论** 动态链接器在程序启动阶段完成所有符号解析,GOT表在首次函数调用前已填充完毕,体现了现代系统的优化特性。 ### 5.8 本章小结 本章完成了从汇编代码到可重定位目标文件的转换分析。通过执行gcc -c hello.s -o hello.o命令,生成了2.3KB的ELF可重定位文件。深入分析了hello.o的ELF格式,包括代码段(.text)202字节、只读数据段(.rodata)147字节以及11个重定位项。 汇编阶段将hello.s中的汇编指令转换为机器码,但所有外部符号引用(如puts、printf等函数调用和字符串地址)均未解析,使用0占位符表示。这些未解析的引用被记录在重定位表中,为链接阶段的地址绑定提供了必要信息。 通过对比hello.s和hello.o,观察到汇编器完成了标签到具体地址的转换、相对跳转偏移量的计算等工作。然而,机器语言中的函数调用和外部数据引用仍保持未绑定状态,这体现了可重定位目标文件的核心特点:代码已编译完成,但最终内存地址需要链接器确定。 ## 第6章 hello进程管理 ### 6.1 进程的概念与作用 进程是计算机中正在运行的程序实例,是操作系统进行资源分配和调度的基本单位。每个进程拥有独立的地址空间、代码、数据和系统资源。进程的主要作用包括: 1.资源隔离:每个进程有独立的虚拟地址空间,防止相互干扰 2.并发执行:多个进程可以同时运行,提高系统利用率 3.保护机制:操作系统通过进程实现访问控制和保护 4.状态管理:进程有明确的生命周期状态(创建、就绪、运行、阻塞、终止) ### 6.2 简述壳Shell-bash的作用与处理流程 **Shell-bash** **的主要作用:** 1.命令行解释:解析用户输入的命令和参数 2.进程创建:通过fork()和execve()创建新进程 3.输入输出重定向:管理标准输入、输出、错误流 4.管道连接:连接多个命令的输入输出 5.作业控制:管理前台和后台进程 6.环境管理:维护环境变量和工作目录 **Shell** **处理流程:** 读取命令 → 解析命令 → 查找可执行文件 → fork创建子进程 → execve加载程序 → wait等待完成 ### 6.3 Hello的fork进程创建过程 **1. Shell** **接收命令** $ ./hello **2. Shell** **调用fork()系统调用** pid_t pid = fork(); fork()执行效果: 创建当前进程的完整副本 子进程获得父进程的代码、数据、堆栈的副本 父子进程从fork()返回处继续执行 父进程获得子进程PID,子进程获得0 **3.** **子进程调用execve()** execve("./hello", argv, environ); execve()执行效果: 删除子进程现有的虚拟内存段 加载hello程序到内存 设置新的代码段、数据段、堆栈段 跳转到hello的入口点_start **4.** **父进程调用wait()** waitpid(pid, \&status, 0); 父进程等待子进程hello执行完毕。 ### 6.4 Hello的execve过程 **execve** **系统调用的详细过程:** **1.** **参数传递** // execve调用参数 execve("./hello", // 程序路径 argv, // 参数数组 \["hello", NULL

environ); // 环境变量

2. 加载程序

内核检查文件类型和权限

读取ELF文件头,验证有效性

为程序分配新的虚拟地址空间

3. 内存映射

映射内容:

0x555555554000-0x555555555000: 只读段(程序头、动态链接信息)

0x555555555000-0x555555556000: 代码段(可执行)

0x555555556000-0x555555557000: 只读数据段

0x555555557000-0x555555558000: 读写数据段

4. 动态链接器设置

将动态链接器路径写入.interp段

设置动态链接器为初始程序入口(如果需要)

传递辅助向量(auxiliary vector)

5. 栈设置

栈内容(从高地址向低地址):

环境变量字符串

命令行参数字符串

环境变量指针数组

命令行参数指针数组

argc值

6.5 Hello的进程执行

进程上下文切换过程:

  1. 进程控制块(PCB)包含:

进程ID、状态、优先级

寄存器保存区域

内存管理信息

文件描述符表

信号处理信息

2. 上下文切换步骤:

保存当前进程上下文 → 选择下一个进程 → 恢复目标进程上下文 → 更新地址空间 → 设置程序计数器

3. 用户态与核心态转换:

系统调用时(如sleep、write):

用户态 → 保存寄存器 → 核心态(执行系统调用)→ 恢复寄存器 → 用户态

hello 进程的时间片管理:

1.时间片分配:Linux默认时间片约100ms

2.调度策略:hello作为交互式进程,使用CFS调度器

3.优先级:普通优先级(nice值0)

6.6 hello的异常与信号处理

1. hello 执行中可能遇到的异常:

|------|-------------------|-------------|
| 异常类型 | 产生原因 | 处理方式 |
| 系统调用 | printf、sleep等函数调用 | 陷入内核,执行系统调用 |
| 缺页异常 | 访问未映射的虚拟内存 | 分配物理页,更新页表 |
| 保护异常 | 非法内存访问 | 发送SIGSEGV信号 |
| 算术异常 | 除零等算术错误 | 发送SIGFPE信号 |

2. hello 可能接收的信号:

|---------|---------|--------------|
| 信号 | 产生方式 | 默认处理 |
| SIGINT | Ctrl+C | 终止进程 |
| SIGTSTP | Ctrl+Z | 暂停进程 |
| SIGCONT | fg命令 | 继续执行 |
| SIGSEGV | 非法内存访问 | 终止并core dump |
| SIGCHLD | 子进程状态变化 | 忽略 |

3. 实际测试命令和结果:

测试1:正常运行

图6-1 程序正常运行输出

测试2:Ctrl+C中断

图6-2 Ctrl+C中断程序

测试3:Ctrl+Z暂停

图6-3 Ctrl+Z暂停程序并查看状态

jobs命令

ps命令

pstree命令

fg命令(恢复前台)

测试4:查看进程信息

图6-4 查看后台进程信息

终端1:

$ ./hello &

终端2:

$ ps aux | grep hello

benjamin 1234 0.0 0.0 1234 567 pts/0 S 10:00 0:00 ./hello

pstree -p $

bash(1000)─┬─hello(1234)

└─pstree(1235)

测试5:发送信号

图6-5 使用kill命令发送信号、使用kill -STOP暂停进程、使用kill -CONT恢复进程、使用kill -TERM终止进程

发送SIGTERM终止进程

发送SIGSTOP暂停进程

发送SIGCONT恢复进程

6.7本章小结

本章深入分析了hello程序的进程管理机制。首先阐述了进程的基本概念与作用,说明了shell如何通过fork-execve机制创建hello进程。通过实际测试验证了hello在运行过程中可能遇到的各类异常和信号,包括SIGINT(Ctrl+C)、SIGTSTP(Ctrl+Z)等。

通过前台运行、后台执行、键盘信号测试等多种方式,验证了hello程序对Unix标准信号的响应机制。测试结果表明,程序能正确处理Ctrl-C(立即终止)、Ctrl-Z(暂停挂起)等键盘信号,并能通过jobs、ps、pstree、fg、kill等命令进行进程状态查看和控制管理。

实验发现hello程序存在后台执行限制:当在后台运行并尝试读取终端输入(getchar())时,会被shell自动挂起,显示为"suspended (tty input)"状态。这一机制体现了操作系统对终端访问的保护。

第7章 hello的存储管理

7.1 hello的存储器地址空间

1.逻辑地址:程序中使用的地址,如&student_id、&i等局部变量地址

2.线性地址:在x86-64架构中,段式管理基本不起作用,线性地址通常等于逻辑地址

3.虚拟地址:进程视角的地址空间,如main函数的0x5555555551c9

4.物理地址:实际DRAM芯片上的地址,通过MMU转换得到

hello 程序地址空间示例:

代码段:0x555555555000-0x555555556000

数据段:0x555555557000-0x555555558000

堆栈段:0x7ffffffde000-0x7ffffffff000

7.2 Intel逻辑地址到线性地址的变换-段式管理

x86-64 架构中段式管理的简化:

1.段寄存器作用减弱:CS、DS、ES、SS等段寄存器仍存在,但通常设置为0

2.平坦内存模型:Linux使用平坦内存模型,段基址为0,界限为4GB

3.逻辑地址转换:逻辑地址 = 段选择子 + 偏移 → 线性地址

4.实际实现:段基址为0,因此线性地址 = 偏移地址

对于hello程序:

所有段(代码、数据、堆栈)基址均为0

逻辑地址直接映射为线性地址

分段主要用于权限检查而非地址转换

7.3 Hello的线性地址到物理地址的变换-页式管理

7.3.1 页表结构

线性地址 → 页表查询 → 物理地址

7.3.2 页大小与对齐

标准页大小:4KB(4096字节)

hello各段按4KB对齐:

代码段:0x555555555000(页对齐)

数据段:0x555555557000(页对齐)

7.3.3 转换过程

假设hello访问变量student_id:

1.虚拟地址:假设为0x7ffffffde1c0

2.查页表获取物理页框号

3.加上页内偏移得到物理地址

7.4 TLB与四级页表支持下的VA到PA的变换

7.4.1 四级页表结构

虚拟地址:[63:48]保留 | [47:39]L4 | [38:30]L3 | [29:21]L2 | [20:12]L1 | [11:0]偏移

7.4.2 地址转换流程

虚拟地址 → TLB查询(命中) → 物理地址

↓(未命中)

四级页表遍历 → 物理地址

7.4.3 TLB (转换后备缓冲区)

1.缓存最近使用的页表项

2.hello程序的特点:

局部性强:循环访问相同变量

TLB命中率高:减少页表遍历开销

7.4.4 hello 的地址转换示例

访问main函数地址0x5555555551c9:

1.计算各级索引:

L4: (0x5555555551c9 >> 39) & 0x1FF

L3: (0x5555555551c9 >> 30) & 0x1FF

L2: (0x5555555551c9 >> 21) & 0x1FF

L1: (0x5555555551c9 >> 12) & 0x1FF

2.逐级查询页表

3.获取物理页框号

4.加上偏移0x1c9

7.5 三级Cache支持下的物理内存访问

7.5.1 缓存层次结构

CPU → L1 Cache → L2 Cache → L3 Cache → 主存

7.5.2 hello 的缓存访问模式

1.指令缓存:循环执行相同代码,高命中率

2.数据缓存:

student_id、student_name:访问频繁

循环变量i:频繁更新

3.时间局部性:循环体反复执行

4.空间局部性:数组元素连续存储

7.5.3 缓存行大小影响

典型缓存行:64字节

hello的数据特点:

1.student_id[11]:11字节 + 1终止符

2.student_name[7]:7字节(UTF-8中文)

3.可放入单个缓存行

7.6 hello进程fork时的内存映射

7.6.1 fork() 的内存管理

pid_t pid = fork(); // Shell创建hello进程

7.6.2 写时复制(Copy-on-Write)

1.初始状态:子进程共享父进程页表

2.写入时:发生缺页异常,复制物理页

3.对hello的意义:

快速创建进程

实际内存复制推迟到写入时

7.6.3 fork 后的内存布局

父进程(bash)和子进程(hello):

1.共享代码段(只读)

2.独立的数据段、堆、栈

3.独立的页表指向相同物理页(初始时)

7.7 hello进程execve时的内存映射

7.7.1 execve 内存操作

execve("./hello", argv, environ);

7.7.2 内存映射步骤

1.释放旧内存:清除原进程的用户空间

2.映射新程序:根据ELF程序头建立映射

3.设置堆栈:分配栈空间,设置参数和环境变量

7.7.3 hello 的ELF段映射

|---------|----------------|-----|--------|
| ELF段 | 虚拟地址范围 | 权限 | 文件偏移 |
| .interp | 0x555555554318 | r-- | 0x318 |
| .text | 0x555555555000 | r-x | 0x1000 |
| .rodata | 0x555555556000 | r-- | 0x2000 |
| .data | 0x555555557000 | rw- | 0x3000 |

7.7.4 动态链接库映射

libc.so.6:0x7ffff7dbf000

ld-linux-x86-64.so.2:0x7ffff7fcf000

7.8 缺页故障与缺页中断处理

7.8.1 缺页类型

hello可能遇到的缺页:

1.首次访问缺页:

第一次访问代码/数据页

从磁盘加载到内存

2.写时复制缺页:

fork后首次写入共享页

复制物理页

3.访问权限缺页:

写入只读页(如.rodata)

触发段错误(SIGSEGV)

7.8.2 缺页处理流程

缺页异常 → 陷入内核 → 查找vma → 分配物理页 → 加载数据 → 更新页表 → 返回用户态

7.8.3 hello 的具体情况

启动阶段:多个缺页加载代码和数据

执行阶段:栈增长可能触发缺页

循环执行:TLB和缓存减少缺页

7.9动态存储分配管理

7.10本章小结

本章深入分析了hello程序的存储管理机制。从逻辑地址、线性地址、虚拟地址到物理地址的转换过程,详细阐述了x86-64架构下的段式管理、页式管理、TLB和四级页表工作机制。

通过分析hello的内存布局和访问模式,理解了缓存层次结构对程序性能的影响。进程创建时的fork-execve机制涉及复杂的内存映射和写时复制技术,这些机制在保证效率的同时实现了进程间的隔离保护。

缺页异常处理机制确保了按需加载和内存高效利用,而动态存储分配则为库函数提供了灵活的内存管理能力。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.1.1 设备文件化

1.一切皆文件:设备被抽象为特殊文件

2.设备文件位置:/dev目录下

h3.ello涉及的设备:

终端设备:/dev/tty 或 /dev/pts/*

标准输入:/dev/stdin

标准输出:/dev/stdout

8.1.2 设备类型

1.字符设备:终端、键盘(按字符流访问)

2.块设备:磁盘(按块访问)

3.网络设备:网卡(特殊套接字接口)

8.1.3 设备驱动模型

应用程序(hello) → 标准库(glibc) → 系统调用 → 虚拟文件系统 → 设备驱动 → 硬件设备

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

Unix提供统一的IO接口,hello使用了以下关键函数:

// hello使用的标准IO函数

printf() // 格式化输出到stdout

getchar() // 从stdin读取字符

8.2.2 底层系统调用

|-----------|---------|------------|
| 标准库函数 | 对应系统调用 | 功能 |
| printf() | write() | 写入数据到文件描述符 |
| getchar() | read() | 从文件描述符读取数据 |
| puts() | write() | 写入字符串并换行 |

8.2.3 文件描述符

hello使用的文件描述符:

0:标准输入(stdin)

1:标准输出(stdout)

2:标准错误(stderr)

8.2.4 IO 缓冲机制

1.全缓冲:磁盘文件(hello未使用)

2.行缓冲:终端输出(printf使用)

3.无缓冲:标准错误

hello的缓冲特点:

printf输出到终端:行缓冲

遇到换行符\n时刷新缓冲区

程序正常退出时自动刷新

8.3 printf的实现分析

8.3.1 printf 函数调用链

printf() → vprintf() → vsprintf() → write() → sys_write() → 终端驱动

8.3.2 具体实现步骤

步骤1:格式化处理

// 类似vsprintf的处理

char buffer[1024];

va_list args;

va_start(args, format);

len = vsprintf(buffer, format, args);

va_end(args);

步骤2:系统调用

// 调用write系统调用

write(STDOUT_FILENO, buffer, len);

步骤3:陷入内核

用户态 → int 0x80/syscall → 内核态 → sys_write()处理

步骤4:终端驱动处理

1.检查终端类型和模式

2.处理特殊字符(如\n转\r\n)

3.写入终端缓冲区

步骤5:显示到屏幕

终端驱动 → 显示控制器 → VRAM → 显示器

字符到字模库查找字形

写入视频内存(VRAM)

显示器按刷新率扫描显示

8.3.3 hello 中printf的使用

printf("第%d次:Hello %s %s\n", i+1, student_id, student_name);

处理过程:

1.解析格式字符串:%d、%s、%s

2.转换整数i+1为字符串

3.拼接所有字符串

4.写入标准输出

8.4 getchar的实现分析

8.4.1 getchar 函数调用链

getchar() → read() → sys_read() → 终端驱动 → 键盘中断

8.4.2 键盘中断处理

中断触发:

1.用户按键产生扫描码

2.键盘控制器发送中断请求

3.CPU响应中断,执行中断处理程序

中断处理流程:

键盘中断 → 读取扫描码 → 转换为ASCII码 → 存入缓冲区

8.4.3 read 系统调用

// getchar内部实现

int getchar(void) {

char c;

if (read(STDIN_FILENO, &c, 1) == 1)

return (unsigned char)c;

return EOF;

}

8.4.4 终端输入模式

hello运行时终端的典型设置:

规范模式:行缓冲,等待回车

回显:显示键入的字符

信号处理:Ctrl-C等特殊字符处理

8.4.5 hello 的getchar行为

getchar(); // 等待用户按回车键

具体过程:

1.程序阻塞,等待输入

2.用户按键存入系统缓冲区

3.按回车键时,整行数据传递给程序

4.getchar返回第一个字符(或EOF)

8.4.6 信号中断处理

如果getchar等待时收到信号:

SIGINT(Ctrl-C):返回EINTR错误

SIGTSTP(Ctrl-Z):进程暂停,恢复后继续等待

8.5本章小结

本章深入分析了hello程序的IO管理机制。Linux通过统一的文件接口管理所有IO设备,hello程序使用的printf和getchar函数最终通过write和read系统调用与终端设备交互。

printf的实现涉及复杂的格式化处理、缓冲区管理和终端驱动交互,最终将字符显示到屏幕。getchar的实现则依赖于键盘中断处理、终端驱动和系统调用机制,实现了从键盘到程序的字符传输。

通过分析这些IO操作的底层机制,理解了用户程序如何通过操作系统提供的抽象接口与物理设备交互,以及操作系统如何管理设备资源、处理中断和提供统一的访问接口。这些机制保证了hello程序能够正常地从用户获取输入并向用户显示输出。

结论

1. 从程序到进程(P2P过程)

编写阶段:创建hello_linux.c源文件,包含main函数及字符串数据

预处理阶段:gcc -E展开头文件,删除注释,生成hello.i

编译阶段:gcc -S将C代码转换为x86-64汇编代码hello.s

汇编阶段:gcc -c将汇编代码转换为机器码,生成可重定位目标文件hello.o

链接阶段:gcc链接hello.o与C库,解析符号引用,生成可执行文件hello

加载执行:shell通过fork()+execve()创建进程,内核加载hello到内存并执行

2. 从零到零(020过程)

零状态:hello作为ELF文件静默存储在磁盘上

进程创建:shell解析命令,fork子进程,execve加载hello

执行阶段:进程在CPU上运行,调用printf、sleep、getchar等函数

终止阶段:main返回0,进程终止,资源回收

归零状态:进程消失,仅保留磁盘上的可执行文件

3. 关键转换过程

文本到二进制:C源码 → 汇编 → 机器码 → 可执行文件

静态到动态:磁盘文件 → 内存映像 → 执行状态

用户到内核:系统调用在用户态与内核态间切换

虚拟到物理:虚拟地址空间通过页表映射到物理内存

计算机系统的设计让我体会到分层抽象的精妙------每层隐藏下层复杂度,为上层提供简洁接口。从hello程序看,我们写C代码时不用管汇编细节,编译器自动优化;运行时操作系统管理内存和进程,我们只需关注逻辑。这种"各司其职"的设计让复杂系统可控。

我的创新想法是让系统更智能适应。现在的优化多是固定的,比如缓存大小、调度策略。如果系统能观察程序行为自动调整呢?比如发现hello在循环打印,就预缓存字符串;看到用户频繁暂停,就调整进程切换策略。

另一个方向是跨层信息传递。编译器知道程序结构(比如哪些数据只读),但操作系统不知道。如果编译器能传递这些信息,系统可以更好优化------把只读数据放在特殊内存区,对循环代码使用更积极的缓存策略。

这些创新不是重造系统,而是在现有基础上增加"感知-适应"能力,让系统从"一刀切"变得更贴合具体程序的需求。

附件

hello_linux.c - C语言源代码文件

hello.i - 预处理后文件(展开头文件)

hello.s - 汇编代码文件

hello.o - 可重定位目标文件(未链接)

hello - 最终可执行文件

hello_elf.txt - hello的ELF格式分析

hello_o_elf.txt - hello.o的ELF格式分析

hello_disasm.txt - hello反汇编代码

hello_o_disasm.txt - hello.o反汇编代码

hello_verbose - gcc详细编译日志

参考文献

1\] Randal E. Bryant, David R. O'Hallaron. 深入理解计算机系统(原书第3版)\[M\]. 龚奕利,寇建译. 北京:机械工业出版社,2016. ISBN: 9787111544937. \[2\]俞甲子,石凡,潘爱民. 程序员的自我修养:链接、装载与库\[M\]. 北京:电子工业出版社,2009. \[3\] Daniel P. Bovet, Marco Cesati. 深入理解Linux内核(原书第3版)\[M\]. 北京:中国电力出版社. ISBN: 9787508353944. \[4\] Michael Kerrisk. The Linux Programming Interface: A Linux and UNIX System Programming Handbook\[M\]. San Francisco: No Starch Press, 2010. ISBN: 9781593272203. \[5\] John R. Levine. Linkers and Loaders\[M\]. San Francisco: Morgan Kaufmann, 1999. ISBN: 9781558604964. \[6\][深入理解计算机系统------知识总结-CSDM博客](https://blog.csdn.net/belle_mei/article/details/123914765?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522b71d6b4c8d62386270e4518257d7b211%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=b71d6b4c8d62386270e4518257d7b211&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-1-123914765-null-null.142%5ev102%5econtrol&utm_term=%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F%E2%80%94%E2%80%94%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93&spm=1018.2226.3001.4187 "深入理解计算机系统——知识总结-CSDM博客")

相关推荐
A666666666678901 小时前
程序人生-Hello’s P2P
c语言·程序人生·microsoft
yuezhilangniao1 小时前
程序人生-杂谈-简单对比一下 学霸和linux科学设计
linux·程序人生·职场和发展
一谦的视界1 小时前
大学新生,如何轻松入门编程成为雷布斯
程序人生·职场和发展·程序员创富
hit_waves2 小时前
程序人生-Hello’s P2P 哈尔滨工业大学计算机系统大作业
c语言·程序人生·课程设计·p2p·大作业
hoperest2 小时前
程序人生-Hello‘s P2P
linux·c语言·程序人生·ubuntu
quixoticalYan2 小时前
哈工大计算机系统大作业报告-程序人生-Hello’s P2P
linux·windows·程序人生·ubuntu·课程设计
2401_876381922 小时前
程序人生-Hello’s P2P
数据库·程序人生·p2p
wxgl_xyx2 小时前
程序人生-Hello’s P2P(2025)
程序人生·职场和发展·p2p
追随者永远是胜利者2 小时前
(LeetCode-Hot100)64. 最小路径和
java·算法·leetcode·职场和发展·go