关注

程序人生-Hello’s P2P

摘  要

本论文以简单的“Hello World”程序(hello.c)为研究对象,系统性地追踪和分析了其在Linux系统(x86-64架构)中从源代码到进程终止的完整生命周期。通过结合理论分析与实践操作,本文深入探讨了程序在计算机系统中的处理过程,涵盖了预处理、编译、汇编、链接等构建阶段,以及进程管理、存储管理和I/O管理等运行时机制。借助GCC、GDB、readelf、objdump等工具,逐步解析了源代码如何被转换为机器指令,操作系统如何为其创建独立的执行环境,以及硬件与软件如何协同工作以执行程序指令并处理输入/输出。本研究不仅验证了计算机系统课程中的核心原理,还通过具体实例揭示了系统各层级间精密复杂的交互与协作,深化了对程序“从程序到进程”(P2P)和“从零到零”(O2O)生命周期的理解。

关键词:计算机系统; 编译链接; 进程管理; 存储管理; I/O管理; ELF格式

自媒体发表截图

目  录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

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

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

6.2 简述壳Shell-bash的作用与处理流程

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

7.1 hello的存储器地址空间

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

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

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

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

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

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

7.9动态存储分配管理

7.10本章小结

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献


第1章 概述

1.1 Hello简介

Hello的P2P(从程序到进程)过程始于hello.c源代码,经过预处理、编译、汇编和链接四个阶段,最终生成可执行文件。当用户在Shell中执行该文件时,操作系统通过fork()创建新进程,再通过execve()加载程序代码,使其成为内存中活动的进程。进程执行时,CPU执行其指令,操作系统管理其内存、I/O和调度,完成打印输出、休眠等待等功能。

Hello的O2O(从零到零)过程则描述了程序从无到有再到无的完整生命周期。初始时程序不存在(零状态);经过编写和编译获得物理存在(到一);运行时在内存和CPU中获得逻辑存在;最终进程终止,所有资源被操作系统回收,回归虚无(回零)。这一循环体现了计算机系统资源的完整分配与回收过程。

1.2 环境与工具

硬件环境:

虚拟机:VMware Workstation

处理器:Intel x86_64架构,2个逻辑核心

内存:7.7 GB

缓存:三级缓存(L1/L2/L3)

软件环境:

操作系统:Ubuntu 24.04.3 LTS

内核版本:Linux 6.14.0-37-generic

系统架构:64位

开发与调试工具:

编译器:GCC 13.3.0

调试器:GDB 15.0.50

分析工具:readelf、objdump、strace、ltrace

系统工具:ps、pstree、top、ldd

1.3 中间结果

如下表:

文件名

作用

hello.c

最初的C语言源代码文件,包含主程序逻辑。

hello.i

预处理后的文本文件,所有宏和头文件已被展开,用于分析预处理阶段的结果。

hello.s

编译生成的汇编语言文件,展示了C代码如何被转换为x86-64汇编指令,用于分析编译优化与代码生成。

hello.o

汇编后生成的可重定位目标文件(ELF格式),包含机器码、符号表和重定位信息,用于分析目标文件结构。

hello

链接后最终生成的可执行文件,可直接在系统中运行,用于分析ELF可执行文件格式、虚拟地址空间和动态链接行为。

hello_o_disassembled.txt

通过objdump -d -r hello.o生成的文件,包含hello.o中代码节(.text)的反汇编结果及重定位信息。用于分析第4章"汇编"阶段,展示机器码与汇编指令的对应关系、重定位条目的具体内容,为理解链接前的可重定位目标文件状态提供关键数据。

hello_all_disassembled.txt

通过objdump -D hello生成的文件,包含可执行文件hello中所有节区(包括代码段、数据段、只读数据段、动态链接信息等)的完整反汇编。用于分析第5章"链接"阶段,展示链接后各节区的最终内存布局、地址绑定结果,以及代码与数据混合的完整二进制映像。

1.4 本章小结

本章作为全文概述,首先介绍了Hello程序的P2P(从程序到进程)与O2O(从零到零)生命周期的核心概念。随后详细说明了完成本研究所依赖的硬件与软件环境,包括虚拟机配置、操作系统版本及关键的开发调试工具链。最后列出了在分析过程中生成的关键中间文件,为后续章节的深入探讨奠定了基础。

(第1章0.5分)


第2章 预处理

2.1 预处理的概念与作用

2.1.1 预处理的概念

预处理是C语言编译过程中的初始翻译阶段,由C预处理器(C Preprocessor)执行。这一阶段在语法分析和代码生成之前进行,专门处理以井号(#)开头的预处理指令,对源代码进行文本级转换和准备。预处理不涉及语法分析或语义检查,而是进行纯文本操作,形成经过完全展开的翻译单元,作为后续编译阶段的输入。

2.1.2 预处理的作用

预处理的主要作用包括:

1.头文件包含:通过#include指令将指定的头文件内容逐字插入到源文件中,形成单一的编译单元。这确保了函数原型、类型定义、宏定义和外部声明的可见性,满足C语言的单遍编译要求。

2.宏替换与展开:对于#define定义的标识符宏和类函数宏,预处理器在宏的作用域内进行词法替换。这包括对预定义宏的展开,以及对宏表达式的求值。

3.条件编译控制:通过#if、#ifdef、#ifndef、#elif、#else和#endif指令组成的条件编译块,基于常量表达式或宏定义状态选择性地包含或排除代码段。

4.删除注释和合并续行:删除所有注释,将物理行通过续行符连接为逻辑行,确保编译器接收的是纯净的代码文本。

5.编译上下文标记:插入行控制指令,为编译器提供准确的源文件名称和行号信息,便于编译错误定位和调试符号生成。

2.2在Ubuntu下预处理的命令

gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC -E hello.c -o hello.i

命令参数说明:

-m64:生成64位目标代码

-Og:进行优化但不影响调试

-no-pie:禁用位置无关可执行文件

-fno-stack-protector:禁用栈保护

-fno-PIC:禁用位置无关代码

-E:仅进行预处理,不进行后续编译

-o hello.i:指定输出文件名为hello.i

预处理过程如图1

1 预处理命令

2.3 Hello的预处理结果解析

2.3.1 预处理结果文件结构分析

如图2,hello.i文件共4030行,相较于hello.c源文件,体积增加了约183倍。这主要是因为三个标准头文件的内容被完全展开插入,且包含大量的嵌套头文件。

2 hello.i行数

2.3.2 头文件展开分析

1. stdio.h的展开

如图3,从第13行开始展开stdio.h头文件,行标记# 1 "/usr/include/stdio.h" 1 3 4表示:重置行号为1当前文件/usr/ include/ stdio.h标志位1 3 4表示新文件开始、系统头文件、extern "C"保护

3 hello.istdio.h的展开

  1. unistd.h的展开

如图4,从第1164行开始展开unistd.h头文件。

4 hello.i中unistd.h的展开

  1. stdlib.h的展开

如图5,从第2720行开始展开stdlib.h头文件。

5 hello.i中stdlib.h的展开

2.3.3 宏定义分析

如图6,执行grep "^#define" hello.i | head -15命令无输出,这表明预处理阶段已经将所有宏展开并替换,hello.i文件中不再包含#define指令,原始头文件中的宏定义已经被替换为实际值。

6 hello.i宏定义分析

2.3.4 条件编译处理

如图7,执行grep -c "#if\|#ifdef\|#ifndef\|#endif" hello.i返回0,说明所有条件编译指令已被处理,预处理器根据当前系统环境选择了合适的代码分支,hello.i文件中只保留了实际编译所需的代码,条件编译指令已被移除。

7 hello.i条件编译处理

2.3.5 原始代码保留情况

如图8,hello.i文件末尾保留了hello.c的原始代码。但是注释完全删除,如图9,验证命令grep -c "//\|/\*" hello.i返回0,确认所有注释已被移除;行号标记插入,# 10 "hello.c" 2表示返回到hello.c文件的第10行,# 11 "hello.c"表示当前为hello.c的第11行;所有C语言语法结构完全保留,包括函数定义、变量声明、控制结构等;原始的#include等预处理指令已被处理,取而代之的是展开的内容和行标记。

8 hello.i在文件末尾保留原始代码

9 hello.i中注释完全删除

2.3.6 预处理过程总结

预处理阶段完成了以下关键转换:

  1. 头文件展开:将stdio.h、unistd.h、stdlib.h的内容完全展开插入
  2. 宏替换:所有宏定义被展开替换,hello.i中不再包含宏定义
  3. 条件编译处理:根据当前平台选择合适分支,移除所有条件编译指令
  4. 注释删除:移除所有单行和多行注释
  5. 行号信息添加:插入行标记以保持原始源代码位置信息

2.4 本章小结

本章通过GCC的预处理命令生成了hello.i文件,并对其内容进行了详细分析。预处理阶段主要完成了以下工作:将三个标准头文件(stdio.h、unistd.h、stdlib.h)的内容完全展开插入到源文件中;保留了所有条件编译指令;删除了所有注释;添加了行号标记。预处理后的hello.i文件是一个独立的、完整的翻译单元,包含了程序所需的所有声明和定义。

(第2章0.5分)


第3章 编译

3.1 编译的概念与作用

3.1.1 编译的概念

编译是C语言编译过程中的核心阶段,它将经过预处理的源代码(.i文件)转换为目标机器架构的汇编语言代码(.s文件)。这一过程由编译器的前端和后端协同完成,遵循严格的翻译阶段序列:首先进行词法分析,将字符流分解为词法单元;接着进行语法分析,根据C语言的上下文无关文法构建抽象语法树;随后进行语义分析,执行类型检查、作用域分析和常量表达式求值;然后生成中间表示;最后经过代码优化和目标代码生成,输出特定指令集架构的汇编代码。编译阶段的输出仍然是文本格式的汇编语言程序,保留了符号信息和相对地址引用。

3.1.2 编译的作用

编译阶段在软件构建过程中承担多重关键作用,具体如下:

1.语法和语义验证:确保源程序符合C语言标准规范,检测类型不匹配、未声明标识符等编程错误。

2.代码优化:实施包括公共子表达式消除、常量传播、死代码删除等转换,在保证语义等价的前提下提升执行效率。

3.目标架构适配:根据指令集特征(如x86-64的寄存器结构、寻址模式)生成高效的底层表示。

4.调试信息生成:建立符号表和调试信息,为错误诊断和源码级调试提供支持。

5. 汇编代码生成:为后续的汇编阶段提供标准化的低级表示,将平台无关的中间代码转换为具体硬件相关的汇编指令序列,生成人类可读的文本格式汇编代码,供汇编器进一步转换为二进制机器码。

3.2 在Ubuntu下编译的命令

gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC -S hello.i -o hello.s

关键参数说明:

-S:指示gcc在编译阶段后停止,生成汇编代码文件

-Og:优化级别,平衡性能与调试友好性

-m64:生成64位代码,对应x86-64架构

其他参数保持与整个编译过程一致

编译命令如图10

10 编译命令

3.3 Hello的编译结果解析

3.3.1 数据类型分析

1.整型变量处理

如图11,对应C代码:int i; i初始化为0,在编译优化后,变量i被优化存储在%ebp寄存器中,而非栈内存,体现了寄存器分配的优化策略。

11 整型变量处理

2.指针类型处理

如图12,指针数组通过基地址加偏移访问,64位系统中每个指针占8字节。

12 指针类型处理

3.字符串常量处理

如图13,编译器将字符串常量分为不同节区存储:

.LC0:中文错误信息,存储为UTF-8编码(八进制转义序列)

.LC1:英文格式化字符串,存储为ASCII编码

13 字符串常量处理

3.3.2 赋值操作分析

1.寄存器赋值

如图14

14 寄存器赋值

2.函数参数赋值

如图15

15 函数参数赋值

3.3.3 算术操作分析

1.自增操作

如图16

16 自增操作

2.类型转换操作

如图17,编译器将atoi(argv[4])优化为strtol(argv[4], NULL, 10),体现了库函数调用的内部优化。

17 类型转换操作

3.3.4 关系操作分析

1.相等比较

如图18

18 相等比较

2.小于等于比较

如图19

19 小于等于比较

3.3.5 控制转移分析

1.if条件结构

如图20

20 if条件结构

2.for循环结构

如图21,其中跳转标签:.L6:错误处理分支(参数错误);.L3:for循环体开始;.L2:for循环条件检查。

21 for循环结构

3.3.6 数组/指针操作分析

1.数组元素访问

如图22,通过基地址%rbx加上固定偏移量访问数组元素,偏移量=索引×8。编译器直接使用固定偏移量,无需运行时计算。

22 数组元素访问

3.3.7 函数操作分析

1.函数调用规范

如图23,参数传递寄存器序列:%rdi, %rsi, %rdx, %rcx, %r8, %r9

23 函数调用规范

2.库函数调用分析

puts替代printf,如图24,当printf只输出字符串常量时,编译器优化为puts调用。

24 puts替代printf

strtol替代atoi,如图25

25 strtol替代atoi

__printf_chk替代printf,编译器使用安全检查版本的printf,防止格式化字符串攻击。

3.函数返回值处理

如图26

26 函数返回值处理

4.main函数返回

如图27

27 main函数返回

3.3.8 编译过程总结

编译阶段完成了以下关键转换:

1.词法与语法分析:验证源代码的语法正确性并构建抽象语法树

2.中间代码优化:在-Og级别下平衡性能与可调试性进行优化

3.目标代码生成:生成x86-64架构特定的汇编指令序列

4.函数调用转换:将高级函数调用转换为遵循ABI约定的低级调用

5.安全增强:自动启用安全检查机制,提升程序安全性

6.控制流转换:将高级控制结构转换为底层跳转指令

3.4 本章小结

本章通过执行GCC编译命令,将预处理后的hello.i文件转换为汇编文件hello.s,并对其进行了详细分析。编译阶段主要实现了从高级C语言到低级汇编语言的转换。编译器首先对代码进行语法语义分析,随后应用多种优化技术,包括寄存器分配优化(循环变量i分配至%ebp寄存器)、函数调用优化(printf替换为__printf_chk)和控制流优化。在代码生成阶段,编译器根据x86-64架构生成高效汇编代码,精确映射C语言结构到机器指令。

(第3章2分)


第4章 汇编

4.1 汇编的概念与作用

4.1.1 汇编的概念

汇编是编译过程的第三个阶段,它将汇编语言程序(.s文件)转换为机器语言二进制程序(.o文件)。这一过程由汇编器(如GNU的as)完成,执行一对一的指令翻译,将人类可读的汇编指令转换为计算机可执行的二进制机器码。

4.1.2 汇编的作用

1.指令翻译:将汇编语言助记符(如mov、add、call)转换为对应的二进制操作码

2.地址解析:将符号标签转换为相对地址或占位符,为后续链接阶段做准备

3.重定位信息生成:标记需要在链接阶段解析的外部引用和地址

4.目标文件生成:创建符合ELF(Executable and Linkable Format)格式的可重定位目标文件

5.符号表构建:记录程序中定义的符号及其属性,供链接器使用

4.2 在Ubuntu下汇编的命令

gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC -c hello.s -o hello.o

命令参数说明:

-c:仅进行编译和汇编,不进行链接

-o hello.o:指定输出文件名为hello.o

其他参数与整体编译选项保持一致

汇编命令如图28

28 汇编的命令

4.3 可重定位目标elf格式

4.3.1 ELF文件头分析

如图29为readelf -h hello.o的输出,ELF头显示hello.o是一个64位小端字节序的可重定位目标文件,使用System V ABI,针对x86-64架构。由于是可重定位文件,入口点地址为0,且没有程序头表。

29 ELF文件头

根据readelf -h hello.o的输出,hello.o文件的ELF头部信息如下表1。

1 ELF头部信息

字段

含义

Magic

7f 45 4c 46 02 01 01 00 ...

ELF文件标识,前4字节为固定魔数(7f 45 4c 46)

Class

ELF64

64位ELF文件

Data

2's complement, little endian

小端字节序

Version

1 (current)

ELF版本1

OS/ABI

UNIX - System V

Unix System V ABI

ABI Version

0

ABI版本0

Type

REL (Relocatable file)

可重定位目标文件

Machine

Advanced Micro Devices X86-64

x86-64架构

Version

0x1

文件版本1

Entry point address

0x0

可重定位文件无入口点

Start of program headers

0 (bytes into file)

无程序头表

Start of section headers

1168 (bytes into file)

节区头表起始偏移

Flags

0x0

处理器特定标志

Size of this header

64 (bytes)

ELF头大小64字节

Size of program headers

0 (bytes)

无程序头表

Number of program headers

0

程序头数量为0

Size of section headers

64 (bytes)

节区头大小64字节

Number of section headers

15

共有15个节区

Section header string table index

14

节区名字符串表索引

4.3.2 节区头表分析

如图30为readelf -S hello.o输出,关键内容如下:

.text节:大小为135字节,包含main函数的机器码

.rodata.str1.8和.rodata.str1.1:存储字符串常量,总大小48+16=64字节

.rela.text节:包含9个重定位条目,用于.text节的地址修正

.rela.eh_frame节:包含1个重定位条目,用于.eh_frame节的地址修正

30 节区头表

根据readelf -S hello.o输出,hello.o包含15个节区,如表2

2 节区头表

节区

类型

地址

偏移

大小

标志

作用

.text

PROGBITS

0x0

0x40

0x87 (135字节)

AX

可执行代码

.rela.text

RELA

0x0

0x310

0xd8 (216字节)

I

.text节的重定位信息

.data

PROGBITS

0x0

0xc7

0x0

WA

已初始化全局变量(空)

.bss

NOBITS

0x0

0xc7

0x0

WA

未初始化全局变量(空)

.rodata.str1.8

PROGBITS

0x0

0xc8

0x30 (48字节)

AMS

8字节对齐的只读字符串

.rodata.str1.1

PROGBITS

0x0

0xf8

0x10 (16字节)

AMS

1字节对齐的只读字符串

.comment

PROGBITS

0x0

0x108

0x2c (44字节)

MS

编译器版本信息

.note.GNU-stack

PROGBITS

0x0

0x134

0x0

栈属性说明

.note.gnu.property

NOTE

0x0

0x138

0x20 (32字节)

A

GNU属性信息

.eh_frame

PROGBITS

0x0

0x158

0x40 (64字节)

A

异常处理帧信息

.rela.eh_frame

RELA

0x0

0x3e8

0x18 (24字节)

I

.eh_frame节的重定位

.symtab

SYMTAB

0x0

0x198

0x138 (312字节)

符号表

.strtab

STRTAB

0x0

0x2d0

0x3d (61字节)

字符串表

.shstrtab

STRTAB

0x0

0x400

0x8a (138字节)

节区名字符串表

4.3.3 重定位表分析

如图31为readlf -r hello.o的输出结果。

31 重定位表

.rela.text节重定位条目(9个)如表3

3 .rela.text节重定位条目

偏移

类型

符号

加数

对应指令位置

0x1a

R_X86_64_32

.rodata.str1.8 + 0

0

加载错误信息字符串地址

0x1f

R_X86_64_PLT32

puts - 4

-4

调用puts函数

0x29

R_X86_64_PLT32

exit - 4

-4

调用exit函数

0x3a

R_X86_64_32

.rodata.str1.1 + 0

0

加载"Hello %s %s %s\n"字符串地址

0x49

R_X86_64_PLT32

__printf_chk - 4

-4

调用__printf_chk函数

0x5c

R_X86_64_PLT32

strtol - 4

-4

调用strtol函数

0x63

R_X86_64_PLT32

sleep - 4

-4

调用sleep函数

0x72

R_X86_64_PC32

stdin - 4

-4

加载stdin全局变量地址

0x77

R_X86_64_PLT32

getc - 4

-4

调用getc函数

.rela.eh_frame节重定位条目(1个):偏移:0x20,类型:R_X86_64_PC32,符号:.text + 0,加数:0

重定位类型说明:

  1. R_X86_64_32:绝对32位地址重定位,用于加载数据段地址
  2. R_X86_64_PLT32:32位PLT相对地址重定位,用于函数调用(通过过程链接表
  3. R_X86_64_PC32:32位PC相对地址重定位,用于PC相对寻址

4.3.4 符号表分析

如图32为readelf -s hello.o的输出结果,符号表包含13个条目。

32 符号表

符号分类:

1. 已定义全局符号(1个):

这是程序中唯一的已定义全局函数。

main:类型FUNC,大小135字节,节区索引1(.text),绑定GLOBAL

值:0x0(在.text节中的偏移)

大小:135字节(与.text节大小一致)

2. 未定义全局符号(7个):

如表4

4 未定义全局符号

符号

类型

绑定

节区索引

说明

puts

NOTYPE

GLOBAL

UND

标准输出函数

exit

NOTYPE

GLOBAL

UND

进程退出函数

__printf_chk

NOTYPE

GLOBAL

UND

安全检查版本的printf

strtol

NOTYPE

GLOBAL

UND

字符串转长整型函数

sleep

NOTYPE

GLOBAL

UND

休眠函数

stdin

NOTYPE

GLOBAL

UND

标准输入文件指针

getc

NOTYPE

GLOBAL

UND

获取字符函数

3. 局部符号(5个):

文件符号:hello.c

节区符号:.text、.rodata.str1.8、.rodata.str1.1

符号表分析要点:

  1. 符号解析状态:

main函数已定义,是程序的入口点所有库函数均为未定义状态,需要链接器解析不存在局部变量符号因为局部变量已优化到寄存器或栈帧中

  1. 符号值与重定位:

所有符号的初始值都为0,因为在可重定位文件中地址尚未确定链接器将根据最终的内存布局为这些符号分配实际地址

3. 符号大小:

main函数大小为135字节,与.text节大小一致未定义符号大小为0,因为它们在当前文件中未定义

4.4 Hello.o的结果解析

4.4.1 反汇编分析

如图33为通过objdump -d -r hello.o命令得到的反汇编结果,根据反汇编结果显示,hello.o文件包含.text节中的135字节机器码。代码结构与第3章的hello.s汇编代码基本对应,但存在以下关键差异:

1. 地址表示方式

hello.s:使用标签(.L2、.L3、.L6)表示跳转目标

hello.o反汇编:使用绝对偏移地址(如<main+0x19>、<main+0x6a>)

2. 外部引用处理

hello.s:直接使用符号名称(如puts、__printf_chk)

hello.o反汇编:使用占位符(00 00 00 00)并附加重定位信息

33 反汇编结果

4.4.2 机器语言与汇编语言映射关系

1.指令编码示例分析

简单指令映射,如图34,单字节指令(55、53)对应简单的寄存器操作,多字节指令包含前缀(48表示64位操作数大小)。

34 简单指令映射

条件跳转指令,如图35,75:jne指令的操作码;0a:相对偏移量10字节(从下一条指令地址0x0f跳到0x19)。

35 条件跳转指令

函数调用指令,如图35,e8:call指令的操作码;00 00 00 00:占位符,链接时填充;重定位条目指示链接器修正这个地址。

36 函数调用指令

4.4.3 关键代码段对照分析

1. 条件判断部分

如图37,汇编器将标签.L6转换为绝对偏移0x19,并计算相对偏移0x0a。

37 条件判断部分

2. 错误处理部分

如图38,字符串地址用0占位,需要R_X86_64_32重定位;call指令目标用0占位,需要R_X86_64_PLT32重定位;重定位信息中的-4偏移是因为PC相对寻址时PC指向下一条指令

38 条件判断部分

3. 循环体部分

如图39,汇编器将相对跳转转换为机器码7e be,其中:7e:jle指令的操作码;be:有符号8位偏移量-66(0xbe = -66);从0x6f(下条指令地址)跳到0x2d:0x6f - 66 = 0x2d

39 循环体部分

4.4.4 重定位条目与机器码对应关系

1.重定位类型分析

R_X86_64_32(绝对地址重定位):

如图40,作用是将.rodata.str1.8节的绝对地址加载到%edi寄存器;链接时修正,用字符串的实际地址替换0x00000000。

40 R_X86_64_32(绝对地址重定位)

R_X86_64_PLT32(PLT相对重定位):

如图41,作用是调用动态链接库函数,通过过程链接表(PLT);链接时修正:计算puts函数在PLT中的相对偏移。

41 R_X86_64_PLT32(PLT相对重定位)

R_X86_64_PC32(PC相对重定位):

如图42,作用是加载stdin全局变量的地址;链接时修正:用stdin实际地址与PC的差值替换0x00000000。

42 R_X86_64_PC32(PC相对重定位)

4.4.5 机器语言构成特点

1. 指令格式

x86-64指令采用变长编码:

操作码:1-3字节

前缀:可选,如REX前缀(48表示64位操作)

ModR/M字节:指定寻址模式

SIB字节:用于复杂寻址

立即数/偏移量:指令所需的数据

2. 操作数编码

操作数编码分级示例如图43

48 - REX前缀(64位模式)

8b - mov指令操作码(寄存器到寄存器/内存到寄存器)

4b - ModR/M字节:

  - Mod=01(8位位移)

  - Reg=001(%rcx)

  - R/M=011(%rbx)

10 - 8位位移量(16字节)

43 操作数编码

3. 分支转移处理

短跳转(8位偏移):

如图44,be = -66(有符号补码),当前PC = 0x6f(下条指令地址),目标地址 = 0x6f - 66 = 0x2d。

44 短跳转(8位偏移)示例

函数调用(32位偏移):

如图45,由于目标地址未知,使用0占位并生成重定位条目。

45 函数调用(32位偏移)示例

4.4.6 与hello.s的详细对照

1. 内存访问模式

如图46,偏移量转换为十六进制,16=0x10,8=0x08,24=0x18;并且指令编码不同,movq 16(%rbx),%rcx→48 8b 4b 10;相同的语义,不同的表示层次。

46 内存访问模式对照

2. 参数传递序列

如图47,与hello.s相比hello.i反汇编字符串地址需要重定位(R_X86_64_32);并且安全检查级别2直接编码为立即数;%eax清零(可变参数函数要求);call指令目标需要重定位。

47 参数传递序列对照

4.4.7 总结:汇编到机器码的转换

汇编器完成了以下关键转换:

  1. 符号解析:将标签转换为绝对或相对地址

2. 指令编码:将助记符转换为二进制操作码

3. 操作数编码:将寄存器名、立即数、内存地址转换为二进制形式

4. 重定位信息生成:为未解析的符号创建重定位条目

5. 地址计算:计算跳转偏移量,处理PC相对寻址

机器语言与汇编语言之间存在精确的映射关系,但机器语言使用二进制编码,而非人类可读的文本;并且机器语言包含完整的寻址模式和操作数信息;机器语言对于外部引用,使用占位符和重定位信息;机器语言还精确控制指令长度和编码格式。

4.5 本章小结

本章完成了从汇编代码到目标文件的转换过程分析。通过执行汇编命令,将hello.s文件转换为hello.o可重定位目标文件,并使用readelf和objdump工具对其进行了详细分析。

汇编阶段的核心工作是将人类可读的汇编指令转换为二进制机器码,同时生成ELF格式的目标文件。这一过程包括:指令编码转换、符号解析与标记、重定位信息生成等关键步骤。通过分析hello.o的ELF结构,可以看到可重定位目标文件包含代码节(.text)、数据节(.data/.rodata)、符号表(.symtab)和重定位表(.rela.text)等多个组成部分。

特别值得注意的是,汇编器对于外部引用和绝对地址的处理方式:它将无法确定的地址留空(用0填充),并生成重定位条目,告知链接器在链接阶段如何修改这些位置。这种设计实现了模块化编译,允许分别编译多个源文件后再链接成最终的可执行文件。

对比hello.s源码和hello.o反汇编结果,这些对比展示了汇编语言到机器语言的映射关系,更好的展示了指令编码、寻址模式、重定位机制等底层细节。

(第4章1分)


5链接

5.1 链接的概念与作用

5.1.1 链接的概念

链接是将多个可重定位目标文件(如hello.o)和必要的库文件合并生成可执行目标文件的过程。这一阶段由链接器(如GNU ld)执行,主要任务包括符号解析、地址空间分配和重定位。链接分为静态链接和动态链接两种形式,其中静态链接将库代码直接复制到可执行文件中,而动态链接在运行时通过动态链接器加载共享库。

5.1.2 连接的作用

1. 符号解析:将模块间的符号引用与符号定义关联,建立统一的符号表。

2. 地址空间分配:为所有输入模块的代码和数据段分配运行时内存地址。

3. 重定位:根据统一的内存地址空间,修改符号引用的地址值。

4. 库文件合并:将标准库和用户库中的必要代码合并到可执行文件中。

5. 启动代码集成:链接C运行时启动代码,设置程序的入口点。

6. 动态链接信息生成:为运行时动态链接器提供必要的信息。

5.2 在Ubuntu下链接的命令

如图48,关键组件说明:

ld-linux-x86-64.so.2:动态链接器

crt1.o:C运行时启动代码,包含_start入口点

crti.o和crtn.o:构造函数和析构函数框架

-lc:链接C标准库libc.so

-lgcc:链接GCC运行时库

48 链接命令

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

5.3.1 ELF文件头信息

如图49为readelf -h hello的输出,具体信息如图49。

49 ELF文件头信息

5.3.2 程序头信息(段信息)

如图50为readelf -l hello的输出结果。

50 readelf -l hello的输出结果

程序头表描述了程序在内存中的布局,如表5、表6是各段的关键信息。

可加载段(LOAD段):

5 可加载段(LOAD段)

段类型

虚拟地址

物理地址

文件大小

内存大小

标志

对齐

内容

LOAD

0x400000

0x400000

0x660

0x660

R

0x1000

ELF头、程序头表、动态链接信息

LOAD

0x401000

0x401000

0x26d

0x26d

R E

0x1000

可执行代码段(.text、.plt等)

LOAD

0x402000

0x402000

0xf4

0xf4

R

0x1000

只读数据段(.rodata等)

LOAD

0x403dd8

0x403dd8

0x268

0x278

RW

0x1000

读写数据段(.data、.bss、.got等)

特殊段:

6 特殊段

段类型

虚拟地址

物理地址

文件大小

内存大小

标志

对齐

作用

INTERP

0x4002e0

0x4002e0

0x1c

0x1c

R

0x1

动态链接器路径(/lib64/ld-linux-x86-64.so.2)

DYNAMIC

0x403de8

0x403de8

0x1f0

0x1f0

RW

0x8

动态链接信息表

PHDR

0x400040

0x400040

0x2a0

0x2a0

R

0x8

程序头表自身

NOTE

0x400300

0x400300

0x30

0x30

R

0x8

GNU属性信息

NOTE

0x400330

0x400330

0x20

0x20

R

0x4

ABI标签信息

GNU_PROPERTY

0x400300

0x400300

0x30

0x30

R

0x8

GNU属性信息

GNU_STACK

0x0

0x0

0x0

0x0

RW

0x10

栈属性(可读写,不可执行)

GNU_RELRO

0x403dd8

0x403dd8

0x228

0x228

R

0x1

只读重定位保护

5.3.3 段到节区的映射关系

根据图50的"Section to Segment mapping"输出,各段包含的节区如下表7。

7 段到节区的映射关系

段序号

包含节区及说明

虚拟地址范围

段类型

段2

.interp:动态链接器路径;

.note.gnu.property、.note.ABI-tag:属性信息;

.hash、.gnu.hash:符号哈希表;

.dynsym、.dynstr:动态符号和字符串表;

.rela.dyn、.rela.plt:重定位表

0x400000-0x401000

动态链接相关段

段3

.init:初始化代码;

.plt、.plt.sec:过程链接表;

.text:主程序代码(包含main函数);

.fini:终止代码

0x401000-0x402000

代码段

段4

.rodata:只读数据(字符串常量);

.eh_frame:异常处理帧信息

0x402000-0x403000

只读数据段

段5

.init_array、.fini_array:初始化和终止函数数组;

.dynamic:动态链接信息;

.got、.got.plt:全局偏移表;

.data:已初始化全局变量;

.bss:未初始化全局变量

0x403dd8-0x404050

读写数据段

5.3.4 内存布局总结

程序的内存布局如下图51。

51 内存布局总结

5.3.6 节区详细分析

如图52,从节区头表可见,hello包含30个节区,其中关键节区如下图52。

通过分析hello的ELF格式,可以看到链接器如何将多个目标文件和库文件合并为一个完整的可执行文件,并建立了优化的内存布局,为操作系统加载和执行程序提供了必要的信息。

5.4 hello的虚拟地址空间

如图53为gdb info proc mappings的输出。

53 gdb info proc mappings输出

5.4.1 虚拟地址空间映射分析

如图54

54 虚拟地址空间映射分析

5.4.2 与5.3节ELF分析的对照分析

如表8读写数据段地址偏移:ELF程序头:LOAD段4,地址0x403dd8,标志RW(可读写);进程映射:0x404000-0x405000,权限rw-p(可读写)。

分析原因为起始地址从0x403dd8调整为0x404000,因为操作系统按4KB页对齐。

8 ELF程序头与进程映射对比表

ELF程序头信息

实际进程映射

差异分析

LOAD段1:0x400000,大小0x660,标志R

0x400000-0x401000,权限r--p

完全一致

LOAD段2:0x401000,大小0x26d,标志R E

0x401000-0x402000,权限r-xp

完全一致

LOAD段3:0x402000,大小0xf4,标志R

0x402000-0x403000,权限r--p

完全一致

LOAD段4:0x403dd8,大小0x268/0x278,标志RW

0x404000-0x405000,权限rw-p

存在地址偏移

内存布局验证:

根据图52和图53,通过readelf -S hello查看各节的实际虚拟地址,并与进程映射对照,得到如表9所示。

5.4.3 关键发现与差异分析

1. 页对齐导致的地址调整

ELF中的LOAD段4起始于0x403dd8,但在进程映射中起始于0x404000。这是因为x86-64架构使用4KB(0x1000)内存页,并且操作系统要求所有内存映射必须从页边界开始,0x403dd8不是页边界,因此向上调整到0x404000。

2. 安全机制引起的权限变化

在ELF中,某些节区(如.got.plt)标记为可写,但在实际进程映射中可能变为只读,因为现代Linux系统启用RELRO(Relocation Read-Only)保护,GOT表在动态链接完成后被设置为只读,防止攻击者通过覆盖GOT表项进行攻击。

3. 地址空间布局随机化(ASLR)

除了程序本身外,进程映射还显示了共享库的随机化地址,libc.so.6加载到0x7ffff7c00000附近,ld-linux-x86-64.so.2加载到0x7ffff7fc5000附近,栈位于0x7ffffffde000-0x7ffffffff000。

5.4.5 结论

通过对比hello程序的ELF分析和实际进程的虚拟地址空间映射,可以得出以下结论:

  1. 映射基本一致:操作系统基本按照ELF程序头表的指示加载程序
  2. 页对齐优化:实际映射按4KB页对齐,可能导致地址偏移
  3. 安全加固:现代操作系统实施比ELF要求更严格的安全策略
  4. 动态调整:动态链接器根据系统安全策略调整内存布局

5.5 链接的重定位过程分析

如下图55为objdump -d -r hello输出结果。

55 objdump -d -r hello输出结果

5.5.1 hello与hello.o反汇编对比分析

通过对比objdump -d -r hello和objdump -d -r hello.o的输出,可以清晰看到链接器如何解决符号重定位问题,如表9。

9 对比分析表

指令类型

hello.o中的情况

hello中的情况

重定位类型

处理结果

字符串常量引用

mov $0x0,%edi

mov $0x402008,%edi

R_X86_64_32

替换为绝对地址

函数调用

call 0x0

call 401090 <puts@plt>

R_X86_64_PLT32

替换为PLT相对偏移

格式化字符串

mov $0x0,%esi

mov $0x402038,%esi

R_X86_64_32

替换为绝对地址

全局变量引用

mov 0x0(%rip),%rdi

mov 0x2df4(%rip),%rdi

R_X86_64_PC32

替换为PC相对偏移

5.5.2 具体重定位条目分析

1. 错误信息字符串重定位

如下图56,重定位过程:链接器将字符串"用法: Hello 学号 姓名 手机号 秒数!\n"合并到.rodata节,分配地址0x402008将绝对地址0x402008写入指令操作数。

56 错误信息字符串重定位

2. puts函数调用重定位

如下图57,重定位过程:链接器创建.plt节,为puts函数创建PLT条目puts@plt,地址0x401090;再计算相对偏移,目标地址 - 下一条指令地址 = 0x401090 - 0x4011f9 = -0x169;编码为32位有符号补码,0xfffffe97(小端序:97 fe ff ff),指令中的e8 97 fe ff ff表示相对偏移-0x169。

57 puts函数调用重定位

3. 格式化字符串重定位

如下图58,重定位过程:链接器将字符串"Hello %s %s %s\n"合并到.rodata节,分配地址0x402038,将绝对地址0x402038写入指令操作数。

58 格式化字符串重定位

4. __printf_chk函数调用重定位

如下图59,重定位过程:链接器创建__printf_chk@plt,地址0x4010b0,计算相对偏移,0x4010b0 - 0x401223 = -0x173,编码为0xfffffe8d(小端序:8d fe ff ff)。

59 __printf_chk函数调用重定位

5. stdin全局变量重定位

如下图60,重定位过程:链接器确定stdin在GOT中的地址为0x404040,计算PC相对偏移,目标地址 - 下一条指令地址 = 0x404040 - 0x40124c = 0x2df4,将偏移0x2df4写入指令。

60 stdin全局变量重定位

5.5.3 链接器的工作流程

链接器执行以下步骤完成重定位:

1. 符号收集与解析收集所有目标文件的符号表建立全局符号表,解析符号引用为每个符号分配运行时地址

2. 节区合并与地址分配合并相同类型的节(所有.text、.data、.rodata等)为合并后的节分配虚拟地址计算每个符号在合并节中的偏移

3. 重定位应用对于每个重定位条目确定符号地址查找符号在全局符号表中的地址计算新值根据重定位类型计算应写入的值修改目标代码将计算值写入指定位置

4. 特殊结构创建创建.plt节用于动态函数调用创建.got.plt节存储动态函数地址设置动态链接相关信息

5.5.4 重定位类型详解

如下表10

10 重定位类型详解

重定位类型

类型说明

适用场景

计算公式

参数说明

R_X86_64_32

绝对地址重定位

加载绝对地址,如字符串常量

S + A

S:符号的实际地址;

A:加数(重定位条目中的addend)

R_X86_64_PLT32

PLT相对重定位

动态库函数调用

L + A - P

L:符号在PLT中的地址;

A:加数(通常为-4);

P:重定位位置的地址

R_X86_64_PC32

PC相对重定位

PC相对寻址,如全局变量访问

S + A - P

S:符号的实际地址;

A:加数;

P:重定位位置的地址

5.5.5 结论

通过对比hello.o和hello的反汇编代码,可以得出以下结论:链接器成功解析了所有外部符号引用,包括函数和全局变量;根据重定位类型正确计算了绝对地址、相对偏移和PC相对偏移;动态链接支持:为动态库函数创建了PLT条目,支持延迟绑定;将多个目标文件(包括启动代码)合并为完整的可执行程序。

5.6 hello的执行流程

5.6.1 使用gdb跟踪执行流程

1.设置断点

如图61

2.运行程序并进入_start

查看_start反汇编代码,如图62

查看寄存器状态,如图63

62 _start反汇编代码

63 寄存器状态

3. 查看调用栈

单步执行几条指令,调用bt指令,查看调用栈,如图64

64 查看调用栈

4. 进入main函数

查看main反汇编代码,如图65

65 main反汇编代码

  1. 函数调用跟踪

第一种情况:参数错误时的执行流程(puts@plt路径)

输入:只有三个输入

此时调用栈以及寄存器状态如图66

66 参数错误时的执行流程

继续执行到exit,程序在exit@plt处暂停,调用栈如图67,继续执行,程序终止,显示错误信息

67 参数错误时的执行流程

第二种情况:参数正确时的执行流程(循环路径)

输入:四个输入

程序在__printf_chk@plt处暂停,__printf_chk的GOT条目以及调用栈如图68,然后程序会多次触发__printf_chk@plt和sleep@plt断点,直到当i=10。

68 循环路径

当i=10时,程序会进入getc@plt,此时调用栈以及寄存器状态如图69,继续执行,程序终止。

69 循环路径结束

5.6.2 hello的执行流程总结

整体程序执行流程如图70

70 hello的执行流程示意图

其中调用与跳转的各个子程序名或程序地址如下表11

11 子程序名程序地址

序号

地址/位置

函数/操作

调用来源

说明

1

0x7ffff7fe4548 (示例)

_dl_start

操作系统加载器

动态链接器入口点

2

0x4010f0

_start

动态链接器

程序入口点

3

0x40110f

call *0x2ec3(%rip)

_start

调用__libc_start_main

4

0x7ffff7de56a0 (示例)

__libc_start_main

_start

C库启动函数

5

0x4011d6

main

__libc_start_main

用户主函数

6

0x4011e0

cmp $0x5,%edi

main

参数个数检查

7

0x4011e3

jne 0x4011ef

main

参数错误跳转

8

0x4011ef

mov $0x402008,%edi

main

加载错误信息地址

9

0x4011f4

call 0x401090

main

调用puts@plt

10

0x4011f9

mov $0x1,%edi

main

设置退出码1

11

0x4011fe

call 0x4010c0

main

调用exit@plt退出

12

0x401240

cmp $0x9,%ebp

main

循环条件检查

13

0x401203

mov 0x10(%rbx),%rcx

main

循环体开始

14

0x40121e

call 0x4010b0

main

调用__printf_chk@plt

15

0x401231

call 0x4010a0

main

调用strtol@plt

16

0x401236

call 0x4010d0

main

调用sleep@plt

17

0x40123d

add $0x1,%ebp

main

循环计数器递增

18

0x401240

cmp $0x9,%ebp

main

再次检查循环条件

19

0x401245

mov 0x2df4(%rip),%rdi

main

循环结束后,准备stdin

20

0x40124c

call 0x4010e0

main

调用getc@plt等待输入

21

0x401251

mov $0x0,%eax

main

设置main返回值0

22

0x401256

ret

main

返回__libc_start_main

23

0x4010c0

call exit@plt

__libc_start_main

程序终止

5.7 Hello的动态链接分析

5.7.1Hello的动态链接分析过程

  1. 分析错误路径的函数(puts和exit)

先设置错误参数,在exit调用处设置断点,运行程序,查看exit的GOT初始值,继续运行到exit调用处,设置监视点观察exit的GOT变化,继续运行,当监视点触发时,记录exit的GOT新值,结果如图71。

71 exit

puts与exit同理,结果如图72

72 puts

  1. 分析正确路径的函数(printf、strtol、sleep、getc)

查看所有GOT初始值,在函数调用处设置断点,运行程序,查看函数的GOT,设置函数的GOT监视点,继续运行,观察函数的GOT何时被修改,都同理,结果如图

73 printf、strtol、sleep、getc

5.7.2 Hello的动态链接分析结果

如表12

12 Hello的动态链接分析结果

函数名

GOT地址

初始值

第一次调用前

第一次调用后

实际地址(十六进制)

变化描述

puts

0x404000

0x401030

0x401030

0xf7c87be0

0x00007ffff7c87be0

从PLT地址变为libc::puts地址

strtol

0x404008

0x401040

0x401040

0xf7c543d0

0x00007ffff7c543d0

从PLT地址变为libc::strtol地址

__printf_chk

0x404010

0x401050

0x401050

0xf7d37990

0x00007ffff7d37990

从PLT地址变为libc::__printf_chk地址

exit

0x404018

0x401060

0x401060

0xf7c47ba0

0x00007ffff7c47ba0

从PLT地址变为libc::exit地址

sleep

0x404020

0x401070

0x401070

0xf7d0ec50

0x00007ffff7d0ec50

从PLT地址变为libc::sleep地址

getc

0x404028

0x401080

0x401080

0xf7c8ef70

0x00007ffff7c8ef70

从PLT地址变为libc::getc地址

5.8 本章小结

本章分析了 hello 程序的链接过程,包括链接的概念、命令、ELF格式、虚拟地址空间、重定位机制、执行流程和动态链接机制。通过 readelf、objdump 和调试工具,深入理解了链接器如何将多个目标文件合并为可执行文件,并支持动态库的延迟绑定。

(第5章1分)


6hello进程管理

6.1 进程的概念与作用

6.1.1 进程的概念

进程是计算机系统中正在执行的程序的实例,它是操作系统进行资源分配和调度的基本单位。每个进程拥有独立的地址空间、堆栈、数据段和代码段,以及系统资源(如文件描述符、信号处理器等)。

6.1.2 进程的作用

进程的作用在于实现程序的并发执行,提高系统资源利用率,同时为程序提供隔离的运行环境,增强系统的稳定性和安全性。

6.2 简述壳Shell-bash的作用与处理流程

Shell是用户与操作系统内核之间的接口程序,bash(Bourne Again Shell)是Linux系统中常用的Shell之一。其主要作用包括:

  1. 命令解释与执行:读取用户输入的命令,解析并执行。
  2. 环境变量管理:维护进程运行所需的环境变量。
  3. 脚本执行:支持执行Shell脚本,实现自动化任务。
  4. 作业控制:支持前后台任务切换、挂起、恢复等。

处理流程:读取用户输入的命令行;解析命令和参数;查找可执行文件(通过PATH环境变量);创建子进程并执行命令;等待命令执行完毕(前台任务)或转入后台执行;最后返回命令执行结果。

6.3 Hello的fork进程创建过程

在执行./hello 2024111641 李金睿 16696135488 3时,Shell会通过fork()系统调用创建一个子进程。具体过程如下:

  1. Shell调用fork(),复制当前进程(Shell进程)的上下文(包括内存、文件描述符等),创建一个几乎完全相同的子进程;
  2. 子进程获得独立的进程ID(PID),父进程为Shell进程;
  3. 子进程通过execve()系统调用加载并执行hello程序;
  4. 父进程(Shell)根据是否在后台运行选择等待子进程结束或继续接收用户命令。

6.4 Hello的execve过程

execve()系统调用用于加载并执行一个新的程序,替换当前进程的地址空间。Hello进程的执行过程如下:

  1. Shell子进程调用execve("./hello", argv, envp);
  2. 操作系统加载hello可执行文件到内存,并解析其ELF格式;
  3. 设置新的代码段、数据段、堆栈段;
  4. 将控制权交给hello程序的main函数;
  5. 传递命令行参数(argv[1]~argv[4])。
  6. Hello进程开始执行后,原有的Shell进程映像被完全替换,但进程ID不变。

6.5 Hello的进程执行

Hello进程在执行过程中,操作系统通过进程调度器分配CPU时间片,实现多进程并发执行。具体过程如下:

  1. 进程上下文:包括寄存器状态、程序计数器、堆栈指针等;
  2. 时间片:每个进程被分配一个时间片(通常几毫秒),时间片用完或被高优先级进程抢占时,发生上下文切换;
  3. 用户态与内核态转换:Hello进程在用户态执行代码;当调用系统调用(如sleep()、getchar())或发生中断时,切换到内核态;内核处理完成后,恢复用户态执行。

6.6 hello的异常与信号处理

6.6.1 异常与信号类型

Hello程序执行过程中可能遇到以下异常及信号:

中断:如Ctrl-C(SIGINT)、Ctrl-Z(SIGTSTP);

陷阱:如调试断点;

故障:如段错误(SIGSEGV);

终止:如非法指令。

6.6.2 信号处理示例

1. 启动Hello程序

如图74

74 启动Hello程序

2. 按Ctrl-Z(SIGTSTP)

如图75,程序被挂起,转入后台。

75 按Ctrl-Z

3. 执行相关命令

ps:查看进程状态

如图76

76 ps

jobs:查看后台作业

如图77

77 jobs

pstree:查看进程树

如图78

78 pstree -p $$

fg:将后台任务调至前台

如图79

79 fg

kill:发送信号终止进程

如图80,当Hello进程处于停止状态(T状态,如按Ctrl-Z后)时:普通终止信号无效,使用默认的kill命令无法终止停止状态的进程;强制终止有效,必须使用kill -9命令才能强制终止进程。

原因分析:进程处于停止状态时,不处理普通信号,SIGTERM信号可以被进程捕获或忽略,SIGKILL信号是强制终止信号,操作系统内核会直接终止进程,不给进程处理机会。

80 kill

4. 按Ctrl-C(SIGINT)

如图81,程序被终止,进程结束。

81 按Ctrl-C

6.7本章小结

本章围绕Hello程序的进程管理展开,从进程的基本概念出发,阐述了Shell的作用与处理流程,详细分析了Hello进程的创建(fork)、加载(execve)、执行与调度过程,并结合用户态与内核态转换、时间片调度等机制,揭示了操作系统对进程管理的内部机制。此外,通过模拟键盘操作与信号处理,展示了Hello进程在运行过程中对异常与信号的响应方式,体现了操作系统对进程控制的灵活性与稳定性。

(第6章2分)


7hello的存储管理

7.1 hello的存储器地址空间

7.1.1 逻辑地址

逻辑地址是程序代码中使用的地址,也称为分段地址。在x86架构中,逻辑地址由两部分组成:段选择子和段内偏移量。出现在机器指令中,是CPU可直接识别的地址形式。格式为段选择子:偏移量,例如CS:0x400500、DS:0x1234。是程序可见的地址,编译器生成的目标代码中使用逻辑地址。

在Hello中的体现为Hello程序中的变量地址、函数地址、指令地址等,如main函数的入口地址、变量i的地址、argv数组元素的地址。

7.1.2 线性地址

线性地址是逻辑地址经过段式管理单元转换后得到的地址。线性地址是平坦地址空间中的连续地址。

转换步骤:CPU从指令中提取段选择子;通过段选择子在全局描述符表或局部描述符表中查找段描述符;从段描述符中获取段基址;将段基址与偏移量相加,得到线性地址。

在现代Linux系统中,分段机制被简化:所有段的基地址都设置为0,段界限设置为整个地址空间,因此,线性地址 = 偏移量。

7.1.3 虚拟地址

在启用分页机制后,线性地址就成为虚拟地址。虚拟地址是进程视角看到的地址空间中的地址,每个进程拥有独立的虚拟地址空间。

虚拟地址关键特征:

  1. 独立性:每个进程有自己的虚拟地址空间,互不干扰
  2. 连续性:虚拟地址空间是连续的(0x00000000到0xFFFFFFFFFFFFFFFF)
  3. 保护性:操作系统通过虚拟地址实现内存保护和隔离
  4. 抽象性:程序员看到的是虚拟地址,无需关心物理内存布局

7.1.4 物理地址

物理地址是实际内存芯片(DRAM)上的地址,对应具体的存储单元。通过MMU和页表将虚拟地址映射为物理地址。Hello的代码段、数据段、堆栈等被映射到物理内存的不同区域。

7.1.5 地址转换关系

如图82

82 地址转换关系流程图

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

7.2.1 段寄存器与段描述符

段寄存器:CS(代码段)、DS(数据段)、SS(堆栈段)、ES、FS、GS

段描述符:定义段的基地址、界限、权限等属性

全局描述符表:包含所有段的描述符

7.2.2 地址转换过程

逻辑地址 = 段选择子:偏移量

线性地址 = 段基址 + 偏移量

具体步骤:

1. 从段寄存器获取段选择子

2. 通过段选择子在GDT/LDT中找到段描述符

3. 从段描述符中读取段基址

4. 段基址 + 偏移量 = 线性地址

在Linux x86-64中,分段被简化所有段的基地址设置为0段界限设置为整个地址空间逻辑地址直接等于线性地址

7.2.3 段式管理示意图

如图83

83 段式管理示意图

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

7.3.1 页式管理基本原理

页面(Page):虚拟内存中的固定大小块(通常4KB)

页框(Page Frame):物理内存中的固定大小块

页表(Page Table):记录虚拟页面到物理页框的映射关系

7.3.2 地址划分

对于48位虚拟地址(x86-64):

13 地址划分

虚拟地址

47-39位

38-30位

29-21位

20-12位

11-0位

PML4索引

PDP索引

PD索引

PT索引

页内偏移

如表13,前四级用于索引页表,最后12位(4KB页)或9位(2MB大页)为页内偏移。

7.3.3 线性地址到物理地址转换示意图

如图

84 线性地址到物理地址转换示意图

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

7.4.1 TLB

作用:缓存最近使用的虚拟地址到物理地址的映射

类型:指令TLB(iTLB)、数据TLB(dTLB)

优点:避免每次地址转换都访问内存中的页表

TLB工作原理:TLB命中,虚拟地址在TLB中找到,直接使用缓存的物理地址;TLB未命中,需要访问页表,找到映射后更新TLB。

7.4.2 Hello程序中的TLB使用

当Hello程序执行时:

代码执行:访问指令地址 → iTLB查找

数据访问:访问变量地址 → dTLB查找

如图85为TLB加速地址转换示意图,图86为四级页表结构(x86-64)

85 TLB加速地址转换示意图

86 四级页表结构

7.4.3 地址转换流程

Hello程序访问变量i的地址:

1.CPU生成虚拟地址

2. 首先查询TLB,命中则直接获得物理地址;未命中则进入页表查找

3. 页表查找(四级):从CR3寄存器获取PML4表基址;用虚拟地址的PML4索引(47-39位)查找PML4条目;获取PDP表基址,用PDP索引(38-30位)查找PDP条目;获取PD表基址,用PD索引(29-21位)查找PD条目;获取PT表基址,用PT索引(20-12位)查找PT条目;从PT条目获取物理页框号;

4. 物理地址 = 物理页框号 × 页大小 + 页内偏移

5. 更新TLB缓存此映射

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

7.5.1 Cache层次结构

现代x86-64系统通常采用三级Cache:

L1 Cache:分为指令缓存(L1i)和数据缓存(L1d),通常各32KB

L2 Cache:统一缓存,通常256KB-512KB

L3 Cache:共享缓存,通常2MB-32MB

7.5.2 Hello程序的内存访问优化

1. 代码访问模式

时间局部性:循环体重复执行,指令在Cache中命中率高

空间局部性:连续指令执行,预取机制有效

87 

2. 数据访问模式

局部变量i在寄存器或L1 Cache中argv数组连续存放,空间局部性好字符串常量位于.rodata段,可能被缓存

7.5.3 Cache访问流程

如图

88 Cache访问流程

7.6 hello进程fork时的内存映射

7.6.1 fork()系统调用的内存管理

当Shell调用fork()创建Hello进程时:创建进程控制块复制父进程(Shell)的PCB分配新的PID虚拟地址空间复制采用写时复制技术父子进程共享相同的物理页框页表条目标记为只读当任一进程尝试写入时,触发页错误,操作系统复制物理页页表复制复制父进程的页表结构页表条目指向相同的物理页框设置COW标志

7.6.2 Hello进程fork时的具体内存映射

如图

如图为fork()时的写时复制机制示意图

89 fork()时的写时复制机制示意图

7.7 hello进程execve时的内存映射

7.7.1 execve()系统调用的内存管理

当子进程调用execve()加载Hello程序时:释放原Shell副本的所有内存映射,清空页表;再创建新的地址空间,根据Hello程序的ELF格式:代码段(只读)从hello文件偏移0x1000处加载,数据段(读写):,从文件偏移0x2000处加载,BSS段(未初始化数据)分配零页;设置内存映射区域:栈区域分配用户栈,设置初始参数,堆区域初始为空,通过brk/sbrk扩展,映射动态链接库。

7.7.2 Hello程序的具体映射

如图90

90 Hello程序的具体映射

7.7.3 页表建立过程

1. 解析ELF头部:确定各段的文件偏移、虚拟地址、大小

2. 建立代码段映射:只读权限,延迟加载

3. 建立数据段映射:读写权限,可能写时复制

4. 建立BSS段:分配零页(全零的物理页)

5. 建立栈映射:分配栈页,设置保护页防止溢出

7.7.4 execve()时的内存映射过程

如图91

91 execve()时的内存映射过程

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

7.8.1 缺页故障类型

Hello程序执行过程中可能遇到的缺页故障:

1. 正常缺页

原因:访问的虚拟页未加载到物理内存

处理:从磁盘加载数据到内存

2. 写时复制缺页

原因:写入COW页

示例:修改从父进程继承的数据页

3. 保护缺页

原因:违反页面权限(如写入只读页)

处理:发送SIGSEGV信号

4. 零页缺页

原因:访问BSS段(未初始化数据)

处理:分配全零的物理页

7.8.2 缺页中断处理流程

CPU访问虚拟地址,MMU转换失败,触发缺页异常,保存现场,切换到内核态,调用缺页中断处理程序,检查缺页原因:如果是页面不在内存则从磁盘加载(I/O操作);如果是COW页则复制物理页,更新页表;如果是零页则分配全零页;如果是保护违规则则发送SIGSEGV;如果是非法地址则发送SIGSEGV。更新页表,设置有效位,恢复现场,返回用户态,最后重新执行触发缺页的指令。

流程图如图92。

92 缺页中断处理流程

7.9动态存储分配管理

7.10本章小结

本章围绕Hello程序的存储管理机制展开,系统阐述了其在Linux系统中的存储运行原理。通过分析虚拟地址空间、段页式地址转换、多级缓存与TLB的协同作用,揭示了Hello程序从逻辑地址到物理内存访问的全过程。同时,结合进程创建中的写时复制、程序加载时的延迟分配,以及缺页中断机制,说明了操作系统如何高效管理内存资源。Hello程序虽小,却完整体现了现代操作系统的存储管理策略,包括内存层次优化、按需加载和多级页表等核心机制,为理解程序在内存中的运行行为提供了具体实例。

(第7章 2分)


8hello的IO管理

8.1 Linux的IO设备管理方法

Linux系统采用统一的设备管理方法,将所有设备都抽象为文件进行处理。在Linux中,无论是硬盘、键盘、显示器还是网络接口,都被视为文件系统中的特殊文件,用户程序可以通过标准的文件操作接口来访问和控制这些设备。

设备管理主要通过Unix IO接口实现,这些接口提供了统一的设备访问方式。在Linux内核中,设备被分为字符设备、块设备和网络设备三大类。字符设备以字节流为单位进行数据传输,如键盘、鼠标等;块设备则以数据块为单位,支持随机访问,如硬盘、U盘等;网络设备则通过套接字接口进行通信。这种设备模型化的方法极大地简化了设备驱动的开发和使用,使得应用程序无需关心底层硬件的具体细节。

在Hello程序的执行过程中,虽然程序本身没有显式的设备操作,但其标准输入输出的实现都依赖于Linux的设备管理机制。当Hello程序调用printf函数输出信息时,实际上是在向标准输出设备文件(通常是终端或控制台)写入数据;当调用getchar函数等待用户输入时,则是在从标准输入设备文件读取数据。

8.2 简述Unix IO接口及其函数

Unix IO接口提供了一组统一的系统调用函数,用于进行输入输出操作。这些函数构成了Linux设备访问的基础,主要包括打开(open)、关闭(close)、读取(read)、写入(write)、定位(lseek)和控制(ioctl)等基本操作。这些函数作用如下:

  1. open函数打开或创建文件/设备,返回一个文件描述符
  2. close函数关闭已打开的文件描述符
  3. read函数从文件描述符读取数据到缓冲区
  4. write函数将缓冲区数据写入文件描述符
  5. lseek函数设置文件偏移量
  6. ioctl函数提供对设备的特殊控制功能。

在Hello程序的执行过程中,虽然程序代码中没有直接调用这些Unix IO接口函数,但C标准库中的输入输出函数(如printf和getchar)最终都会转换为这些系统调用。例如,当Hello程序向终端输出字符串时,printf函数会通过write系统调用将数据写入标准输出设备;当程序等待用户输入时,getchar函数会通过read系统调用从标准输入设备读取数据。这种分层设计使得程序具有更好的可移植性和可维护性。

8.3 printf的实现分析

printf函数的实现是一个从用户空间到内核空间的复杂过程。首先,printf函数内部会调用vprintf或类似函数,这些函数使用可变参数机制处理格式化字符串和参数。printf会将格式字符串和参数传递给vsprintf函数,该函数负责解析格式说明符,并将参数转换为相应的字符串表示形式,生成最终的显示信息字符串。

生成显示信息后,printf通过标准输出流进行输出。在Linux系统中,标准输出通常连接到文件描述符1(STDOUT_FILENO)。当数据准备好后,printf会调用write系统函数将字符串写入标准输出。这一过程涉及用户态到内核态的转换:程序通过陷阱指令发起系统调用,CPU从用户模式切换到内核模式,执行相应的系统调用处理程序。

在内核中,write系统调用将数据传递给字符设备驱动程序。对于显示输出,最终会调用显示设备的驱动子程序。驱动程序首先将ASCII字符转换为字模库中的点阵数据,然后将这些点阵数据写入显示器的视频RAM。VRAM中存储了屏幕上每个像素点的RGB颜色信息,这些信息按照特定的内存布局组织,对应着屏幕上的各个像素位置。

显示芯片按照设定的刷新频率通常为60Hz或更高逐行读取VRAM中的数据,并通过视频信号线向液晶显示器传输每个像素点的RGB分量值。显示器接收到这些信号后,控制液晶分子的排列,从而在屏幕上呈现出相应的字符和图像。整个过程中,从printf调用到最终在屏幕上显示字符,涉及多层软件和硬件的协同工作。

8.4 getchar的实现分析

getchar函数的实现涉及异步异常处理和系统调用机制。当用户在键盘上按下按键时,会触发键盘中断,这是一个异步异常事件。CPU检测到键盘中断后,会暂停当前执行的程序,保存现场信息,然后跳转到预定义的键盘中断处理子程序执行。

键盘中断处理子程序首先从键盘控制器读取扫描码,然后将扫描码转换为对应的ASCII码或特殊键值。这一转换过程需要考虑键盘布局、修饰键状态等因素。转换后的字符码被保存到系统的键盘缓冲区中,这是一个由内核维护的环形缓冲区,用于暂存用户输入的字符。

当Hello程序调用getchar函数时,该函数会通过标准输入流读取数据。在内部,getchar会调用read系统函数,从文件描述符0(STDIN_FILENO)读取字符。read系统调用同样需要通过陷阱指令进入内核模式,内核检查键盘缓冲区中是否有可用的字符数据。如果缓冲区为空,则调用进程会被阻塞,进入睡眠状态,直到有新的键盘输入到来。

当用户输入字符并按下回车键时,键盘中断处理程序将回车键也作为一个字符存入键盘缓冲区。read系统调用检测到换行符后,会将缓冲区中的数据复制到用户空间缓冲区,然后返回读取的字符数。getchar函数从读取的数据中提取第一个字符返回给调用者,如果遇到文件结束或错误,则返回EOF。这个过程体现了操作系统如何处理异步输入事件,以及如何通过阻塞/唤醒机制管理进程的IO操作。

8.5本章小结

本章深入探讨了Hello程序的IO管理机制,从Linux的设备管理方法到具体的输入输出函数实现。Linux将各种设备抽象为文件,通过统一的Unix IO接口进行管理。printf函数的实现涉及格式化处理、系统调用和显示驱动等多个层次,最终通过硬件设备将字符呈现在屏幕上。getchar函数的实现则展示了异步中断处理、缓冲区管理和进程阻塞/唤醒等机制。这些IO管理技术虽然对Hello程序来说是透明的,但它们构成了程序与外界交互的基础,体现了操作系统在管理硬件资源和提供用户接口方面的重要作用。

(第8章 1分)


结论

用计算机系统的语言,逐条总结hello所经历的过程如下

  1. 从文本到令牌预处理

预处理器cpp对hello.c进行文本级操作,处理所有#指令。它将头文件stdio.hunistd.hstdlib.h内容递归插入,展开所有宏定义,移除注释,并添加行号标记,生成hello.i。

2. 从高级语言到低级语言编译

编译器cc1对hello.i进行词法、语法和语义分析,生成中间表示并进行优化(如寄存器分配、函数调用优化)。随后,它将高级C语言结构转换为针对x86-64架构的汇编指令序列,输出到hello.s文件。

3. 从助记符到操作码汇编

汇编器as将hello.s中的汇编指令一对一地转换为机器指令二进制操作码,并解析标签为相对地址。对于外部符号如库函数,它生成占位符和重定位条目,最终输出符合ELF格式的可重定位目标文件hello.o,其中包含代码节(.text)、数据节(.rodata)、符号表(.symtab)等。

4. 从模块到整体链接

链接器ld将hello.o与必要的启动代码和共享库合并。它进行符号解析、节区合并,为所有符号分配运行时虚拟地址,并根据重定位条目修正目标代码中的地址引用。最终生成可执行目标文件hello,包含完整的程序头表、段映射以及动态链接信息。

  1. 从文件到映像加载与执行

用户在Shell中输入命令后,Shell通过fork()系统调用创建子进程。子进程通过execve()系统调用加载hello。操作系统加载器解析hello的ELF头部,将代码段、数据段等映射到进程的虚拟地址空间,设置栈和堆,并将动态链接器ld- linux - x86 - 64.so.2加载到内存。控制权最终传递给hello的入口点_start,进而调用main函数。

  1. 从虚拟到物理运行时内存管理

进程执行时,CPU发出的虚拟地址经由MMU转换。在x86-64 Linux中,分段机制被简化为平坦模型,线性地址等于偏移量。通过四级页表和TLB缓存,虚拟地址被转换为物理地址。内存访问进一步经过L1/L2/L3缓存层次优化。fork时采用写时复制优化内存使用;execve时建立全新的地址空间映射。

  1. 从指令到结果进程调度与I/O

进程由操作系统调度器分配CPU时间片,在用户态执行代码,在调用系统调用(如write, read)或处理中断时陷入内核态。printf通过标准库和write系统调用,最终将字符数据写入视频RAM;getchar通过read系统调用从键盘缓冲区读取数据,涉及异步中断处理。进程接收并处理信号,最终通过exit系统调用终止,所有资源被操作系统回收。

通过本次对Hello程序生命周期的完整剖析,我深切感受到计算机系统是一个极其精密、层次分明的协同工程。从编译器对代码的优化取舍,到链接器对模块的巧妙缝合,再到操作系统对资源的虚拟化与隔离,每一层都在提供抽象的同时隐藏复杂性。这种“抽象与协作”的设计哲学,是系统稳定、高效、安全的基石。

(结论0分,缺失-1分)


附件

完成本大作业过程中生成的所有中间产物文件列表及其作用说明如下表:

14 中间产物的文件名及其作用

文件名

作用

hello.c

最初的C语言源代码文件,包含主程序逻辑。

hello.i

预处理后的文本文件,所有宏和头文件已被展开,用于分析预处理阶段的结果。

hello.s

编译生成的汇编语言文件,展示了C代码如何被转换为x86-64汇编指令,用于分析编译优化与代码生成。

hello.o

汇编后生成的可重定位目标文件(ELF格式),包含机器码、符号表和重定位信息,用于分析目标文件结构。

hello

链接后最终生成的可执行文件,可直接在系统中运行,用于分析ELF可执行文件格式、虚拟地址空间和动态链接行为。

hello_o_disassembled.txt

通过objdump -d -r hello.o生成的文件,包含hello.o中代码节(.text)的反汇编结果及重定位信息。用于分析第4章"汇编"阶段,展示机器码与汇编指令的对应关系、重定位条目的具体内容,为理解链接前的可重定位目标文件状态提供关键数据。

hello_all_disassembled.txt

通过objdump -D hello生成的文件,包含可执行文件hello中所有节区(包括代码段、数据段、只读数据段、动态链接信息等)的完整反汇编。用于分析第5章"链接"阶段,展示链接后各节区的最终内存布局、地址绑定结果,以及代码与数据混合的完整二进制映像。

(附件0分,缺失 -1分)


参考文献

  1.  Randal E. Bryant, David R. O'Hallaron. 深入理解计算机系统(Computer Systems: A Programmer's Perspective)[M]. 北京:机械工业出版社.
  2.  Brian W. Kernighan, Dennis M. Ritchie. C程序设计语言(The C Programming Language)[M]. 北京:机械工业出版社.
  3.  Linux Programmer's Manual: gcc(1), ld(1), execve(2), elf(5).
  4.  GNU Compiler Collection (GCC) Online Documentation. https://gcc.gnu.org/onlinedocs/
  5.  System V Application Binary Interface: AMD64 Architecture Processor Supplement.
  6.  Intel® 64 and IA-32 Architectures Software Developer’s Manual.

(参考文献0分,缺失 -1分)

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/A66666666667890/article/details/156552900

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--