HIT-ICS 2025春计算机系统大作业 程序人生-Hello’s P2P

摘  要

本报告通过分析hello程序从hello.c源代码到进程终止的完整生命周期,系统阐述了计算机系统的多层次协作机制。首先,通过预处理、编译、汇编与链接阶段,将C语言源代码转换为可执行文件;其次,结合进程管理、存储管理及输入输出管理,深入探讨了进程创建、地址空间转换、动态链接、信号处理等核心机制。实验部分利用GCC、GDB、ReadELF等工具,验证了逻辑地址到物理地址的页式转换、动态链接的PLT/GOT机制、异常处理与终端I/O交互等关键技术。本报告通过理论与实践结合,揭示了操作系统、编译器与硬件的协同设计原理,为深入理解计算机系统提供了完整案例。

关键词:预处理;编译;动态链接;进程管理;存储管理;输入输出管理

目  录

第1章 概述… – 4 –

1.1 Hello简介… – 4 –

1.2 环境与工具… – 4 –

1.3 中间结果… – 4 –

1.4 本章小结… – 5 –

第2章 预处理… – 6 –

2.1 预处理的概念与作用… – 6 –

2.2在Ubuntu下预处理的命令… – 6 –

2.3 Hello的预处理结果解析… – 6 –

2.4 本章小结… – 7 –

第3章 编译… – 8 –

3.1 编译的概念与作用… – 8 –

3.2 在Ubuntu下编译的命令… – 8 –

3.3 Hello的编译结果解析… – 8 –

  3.3.1 数据类型与变量… – 8 –

  3.3.2 赋值与表达式… – 9 –

  3.3.3 算术与逻辑操作… – 10 –

  3.3.4 控制转移… – 10 –

  3.3.5 函数操作… – 12 –

  3.3.6 指针与数组操作… – 12 –

  3.3.7 其他操作… – 13 –

3.4 本章小结… – 13 –

第4章 汇编… – 14 –

4.1 汇编的概念与作用… – 14 –

4.2 在Ubuntu下汇编的命令… – 14 –

4.3 可重定位目标elf格式… – 14 –

4.4 Hello.o的结果解析… – 17 –

  4.4.1 反汇编与汇编代码对照分析… – 18 –

  4.4.2 机器语言与汇编语言的映射特点… – 20 –

  4.4.3 关键结论… – 21 –

4.5 本章小结… – 21 –

第5章 链接… – 22 –

5.1 链接的概念与作用… – 22 –

5.2 在Ubuntu下链接的命令… – 22 –

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

5.4 hello的虚拟地址空间… – 27 –

5.5 链接的重定位过程分析… – 29 –

5.6 hello的执行流程… – 32 –

5.7 Hello的动态链接分析… – 34 –

5.8 本章小结… – 35 –

第6章 hello进程管理… – 36 –

6.1 进程的概念与作用… – 36 –

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

6.3 Hello的fork进程创建过程… – 36 –

6.4 Hello的execve过程… – 37 –

6.5 Hello的进程执行… – 37 –

6.6 hello的异常与信号处理… – 37 –

  6.6.1 异常类型与信号生成… – 37 –

  6.6.2 键盘操作与信号响应… – 38 –

6.7本章小结… – 42 –

第7章 hello的存储管理… – 43 –

7.1 hello的存储器地址空间… – 43 –

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

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

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

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

7.6 hello进程fork时的内存映射… – 46 –

7.7 hello进程execve时的内存映射… – 47 –

7.8 缺页故障与缺页中断处理… – 48 –

7.9动态存储分配管理… – 48 –

7.10本章小结… – 49 –

第8章 hello的IO管理… – 50 –

8.1 Linux的IO设备管理方法… – 50 –

8.2 简述Unix IO接口及其函数… – 50 –

8.3 printf的实现分析… – 51 –

8.4 getchar的实现分析… – 52 –

8.5本章小结… – 53 –

结论… – 54 –

附件… – 56 –

参考文献… – 57 –

第1章 概述

1.1 Hello简介

Hello程序的完整生命周期可概括为P2P(Program to Process)和020(Zero to Zero)两个核心过程:

1. P2P(从程序到进程)

预处理(Preprocessing):hello.c通过宏替换、头文件展开和条件编译生成hello.i,消除注释与冗余信息。

编译(Compilation):将hello.i转换为汇编代码hello.s,完成语法语义分析并生成机器无关的低级表示。

汇编(Assembly):将hello.s翻译为机器指令,生成可重定位目标文件hello.o,包含二进制代码和符号表。

链接(Linking):将hello.o与C标准库(如libc.so)动态链接,解析外部符号(如printf),生成可执行文件hello。

进程加载(Process Loading):Shell通过fork创建子进程,execve将hello加载至内存,形成独立进程空间,CPU执行指令直至终止。

2. 020(从零到零)

进程执行结束后,操作系统回收其占用的内存、文件描述符等资源,进程控制块(PCB)被销毁,状态回归初始“零”状态。

1.2 环境与工具

硬件环境:

处理器 13th Gen Intel(R) Core(TM) i7-13700H   2.40 GHz

机带 RAM 16.0 GB (15.7 GB 可用)

系统类型    64 位操作系统, 基于 x64 的处理器

软件环境:Windows11 64位,VMware,Ubuntu

开发与调试工具:Visual Studio;vim objump edb gcc readelf等工具

1.3 中间结果

表1-1 中间结果

文件名称

功能

hello.c

源程序

hello.i

预处理后得到的文本文件

hello.s

编译后得到的汇编语言文件

hello.o

汇编后得到的可重定位目标文件

hello.elf

用readelf读取hello.o得到的ELF格式信息

hello.asm

反汇编hello.o得到的反汇编文件

hello

链接后得到的可执行文件

hello1.elf

用readelf读取hello得到的ELF格式信息

hello1.asm

反汇编hello得到的反汇编文件

1.4 本章小结

本章概述了Hello程序从源代码到进程的完整生命周期(P2P)及其终止后的资源回收(020),明确了预处理、编译、汇编、链接与进程加载的关键步骤。同时,列出了实验所需的软硬件环境与工具,并展示了生成的中间结果文件。后续章节将基于此框架,逐步深入分析每个环节的实现细节。

第2章 预处理

2.1 预处理的概念与作用

预处理是C程序编译过程中的第一阶段,其核心任务是对源代码进行文本级别的处理,为后续的编译阶段提供“纯净”的输入。具体作用包括:

1. 宏替换:处理#define定义的宏,将其替换为实际值或代码片段。

2. 头文件展开:递归展开#include指令,将头文件内容插入源文件中。

3. 条件编译:根据#ifdef、#ifndef等指令选择性保留或删除代码块。

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

5. 添加行号标记:插入#line指令,便于编译器报错时定位原始代码位置。

2.2在Ubuntu下预处理的命令

使用的命令是:gcc -E hello.c -o hello.i

同时,通过ls命令验证文件的存在。

图2-1 预处理

如图2-1所示, hello.i出现在了目录下。

2.3 Hello的预处理结果解析

打开hello.i文件,分析预处理后的代码变化:

图2-2 hello.i

1. 头文件展开

原始代码:

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

预处理结果:

#include指令被替换为对应头文件的全部内容。例如,stdio.h展开后包含printf、getchar等函数的声明,以及FILE、size_t等类型的定义,代码量从几行扩展至近千行。

2. 宏与条件编译处理

原始代码中未定义自定义宏,但标准库中的宏(如NULL)被展开为((void*)0)。

条件编译指令(如#ifdef)未被使用,因此预处理后无变化。

3. 注释删除与代码格式

所有注释(如// 秒数=手机号%5)被完全删除。

代码保留原始缩进结构,但行号因头文件插入而大幅增加。

2.4 本章小结

预处理阶段通过宏替换、头文件展开和注释删除,将原始的hello.c转换为仅包含有效代码的hello.i。此阶段未修改程序的逻辑结构,但为后续编译提供了标准化的输入。生成的hello.i文件是后续编译阶段的起点,其内容直接决定了汇编代码的生成方式。

第3章 编译

3.1 编译的概念与作用

编译是将预处理后的高级语言代码转换为目标机器架构相关的汇编语言代码的过程。其主要作用包括:

1. 语法与语义检查:验证代码是否符合C语言规范,检测类型错误、未声明变量等问题。

2. 中间代码生成与优化:生成抽象语法树(AST)或中间表示(IR),并进行初步优化(如常量折叠、死代码删除)。

3. 汇编代码生成:将优化后的中间代码转换为特定架构(如x86-64)的汇编指令,为后续汇编阶段提供输入。

hello程序中,编译阶段将hello.i文件转换为hello.s,为机器码生成奠定基础。

3.2 在Ubuntu下编译的命令

使用的命令是:gcc -S hello.i -o hello.s

图3-1 编译

如图3-1所示,hello.s出现在了目录下。

3.3 Hello的编译结果解析

3.3.1 数据类型与变量

1. 局部变量

C代码:int i;(未赋初值)

汇编实现:

图3-2 局部变量

变量i存储在栈帧偏移-4(%rbp)处,初始值通过movl $0显式赋0。

subq $32, %rsp为main函数分配32字节栈空间,包含局部变量和参数对齐。

2. 字符串常量

C代码:

“用法: Hello 学号 姓名 手机号 秒数!”

“Hello %s %s %s

汇编实现:

图3-3 字符串常量

字符串常量存储在只读数据段(.rodata),通过标签(如.LC0)引用。编译器将字符串中的汉字编码成UTF-8格式,一个汉字占3个字节。字节之间用分割开。

3.3.2 赋值与表达式

1. 赋值操作

C代码:i = 0(循环初始化)

汇编实现:

图3-4 赋值操作

使用movl指令将立即数0存入变量i的栈位置。

2. 复合赋值操作

C代码:i++

汇编实现:

图3-5 复合赋值操作

addl指令直接在内存中执行加法操作,无需临时寄存器。

3.3.3 算术与逻辑操作

1. 算术操作

C代码:atoi(argv[4])(字符串转整数)

汇编实现:

图3-6 算术操作

这一操作通过指针运算计算argv[4]的地址(argv + 32)。

movq  -32(%rbp), %rax 表示argv指针;

addq  $32, %rax 表示argv[4]地址 = argv + 4*8 =32(64位系统,每个指针占8字节);

movq  (%rax), %rax 意为取argv[4]的值(字符串地址);

movq  %rax, %rdi 表示参数传递:argv[4]字符串地址;

call  atoi@PLT 表示调用atoi转换。

2. 关系操作

C代码:if (argc != 5)

汇编实现:

图3-7 关系操作

cmpl比较argc(存储在-20(%rbp))与立即数5;je根据结果跳转,相等则跳转到.L2,否则继续执行。

3.3.4 控制转移

1. for循环

C代码:for (i=0; i<10; i++)

汇编实现:

图3-8 for循环

本段的核心是循环控制:通过jmp跳过循环体直接检查条件,jle实现“小于等于”跳转。

2. 条件分支(if语句)

C代码:if (argc != 5) { exit(1); }

汇编实现:

图3-9 条件分支

本段的核心是条件跳转:cmpl比较argc与5;je实现“等于”跳转,避免执行下面的错误处理代码。

leaq用于加载错误信息地址;movq表示参数1:字符串地址;call用于输出错误信息;movl表示参数1:退出码1;call表示调用exit(1);.L2表示条件分支结束。

3.3.5 函数操作

1. 参数传递(printf调用)

C代码:printf(“Hello %s %s %s
“, argv[1], argv[2], argv[3])

汇编实现:

图3-10 参数传递

movq  -32(%rbp), %rax 表示 argv指针;

addq  $8, %rax 表示 argv[1]地址;

movq  (%rax), %rsi 表示 argv[1]存入%rsi;

leaq  .LC1(%rip), %rax 表示格式字符串地址存入%rax;

movl  $0, %eax 意为清零%eax,表示无浮点参数;

call  printf@PLT 表示调用printf。

2. 函数返回

C代码:return 0;

汇编实现:

图3-11 函数返回

movl 表示返回值0存入%eax;leave 表示恢复栈帧;ret 表示函数返回。

3.3.6 指针与数组操作

数组访问

C代码:argv[1], argv[2], argv[3]

汇编实现:

图3-12 数组访问

指针运算:通过addq计算数组元素地址(argv + index*8)。addq 表示 argv[1]地址 = argv + 8。

3.3.7 其他操作

1. 隐式类型转换(atoi调用)

C代码:sleep(atoi(argv[4]))

汇编实现:

图3-13 隐式类型转换

类型转换:atoi实现了字符串转整数,返回值(int)直接通过%eax传递给sleep,无显式转换指令。

2. sizeof处理

C代码:未显式使用sizeof,但编译器通过指针偏移(如addq $8)隐式处理指针大小(8字节)。

3.4 本章小结

编译阶段将预处理后的hello.i转换为x86-64架构的汇编代码hello.s,完成了从高级语言到低级指令的转换。编译器通过以下部分实现这一过程:

1. 变量存储:局部变量通过栈帧管理,全局常量存储在只读段。

2. 控制流:条件分支和循环通过cmp和jxx指令实现。

3. 函数调用:参数通过寄存器传递,返回值存储在%eax。

4. 指针与数组:通过地址运算和内存访问实现。

5. 类型转换:隐式处理整数与指针类型,无额外指令开销。

生成的hello.s文件为后续汇编阶段提供了明确的指令集输入,是程序机器码生成的基础。

第4章 汇编

4.1 汇编的概念与作用

汇编是将汇编语言代码(.s文件)转换为机器语言二进制文件(.o文件)的过程。其核心作用包括:

1. 指令翻译:将助记符形式的汇编指令(如movq、call)转换为机器码(二进制编码)。

2. 符号解析:记录代码中未解析的符号(如printf、exit),生成重定位表供链接阶段使用。

3. 生成可重定位目标文件:包含机器码、符号表、重定位信息等,为后续链接提供基础。

4.2 在Ubuntu下汇编的命令

使用的命令是:gcc -c -m64 -no-pie -fno-PIC hello.s -o hello.o

图4-1 汇编

如图4-1所示,hello.o出现在了目录中。

4.3 可重定位目标elf格式

使用readelf -a hello.o > hello.elf命令,获得hello.o文件的ELF格式hello.elf。

图4-2 生成hello.elf

查看hello.elf内容如下:

1. ELF头

ELF头(ELF header)以一个16字节的Magic序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。其余的部分为帮助链接器语法分析和解释目标文件的信息,包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。

图4-3 ELF头

关键字段分析:

Type: REL,表明这是一个可重定位目标文件(非可执行文件)。

Machine: x86-64,目标架构为64位x86。

Entry point: 0x0,可重定位文件无入口地址,需链接后确定。

Section headers: 共14个节头表项,起始位置为文件偏移1160字节。

2. 节头部表

不同节的位置和大小是由节头部表描述的,其中目标文件每个节都有一个固定大小的条目。节头部表包含了文件中出现的各个节的含义,包括节的地址、偏移量、大小等信息。

图4-4 节头部表

关键节分析:

.text:代码段,存放main函数机器指令,大小为0xa3字节(163字节),权限为AX(可分配、可执行)。

.rodata:只读数据段,存放字符串常量(如.LC0和.LC1),大小为0x40字节(64字节)。

.rela.text:代码段的重定位表,链接时需修正.text中的符号地址。

.rela.eh_frame:异常处理框架的重定位表,用于调试信息。

.symtab:符号表,记录全局符号和未定义符号信息。

3. 重定位节

(1).rela.text

重定位节,包含.text节中需要被修正的信息。链接阶段,链接器会把这个目标文件和其他文件组合,结合这个节,修改.text 相应位置的信息。

在本程序中,8 条重定位信息分别是puts、exit、printf、atoi、sleep、getchar和.rodata中的.L0 和.L1。

图4-5 .rela.text

重定位项解析(以printf为例):

Offset: 0x6f,需修正的指令在.text中的偏移地址。

Type: R_X86_64_PLT32,表示使用PLT(Procedure Linkage Table)的相对地址修正。

Sym. Name: printf,符号名,需在链接时解析其实际地址。

Addend: -4,修正值需减去4(与指令编码相关)。

(2).rela.eh_frame

.rela.eh_frame节同.rel.text一样属于重定位信息的section,其中放置.eh_frame 节的重定位信息。.eh_frame节生成描述如何unwind 堆栈的表,用于异常处理(如栈展开)。

图4-6 .rela.eh_frame

重定位项解析:

Type: R_X86_64_PC32,表示相对地址修正。

Sym. Name: .text,目标符号为代码段地址。

Addend: 修正值基于.text节的偏移,此处为+0。

4. 符号表

.symtab节中包含ELF符号表,这张符号表包含一个条目的数组,存放一个程序定义和引用的全局变量和函数的信息。该符号表不包含局部变量的信息。

图4-7 符号表

关键符号分析:

main:类型为FUNC,位于.text节(Ndx=1),大小为163字节,是全局符号(GLOBAL)。

UND符号:如puts、printf等,类型为NOTYPE,节索引为UND,表示需在链接时解析外部符号地址。

局部符号:如.text、.rodata,作用域为LOCAL,用于内部引用。

4.4 Hello.o的结果解析

首先使用命令objdump -d -r hello.o > hello.asm,得到hello.o的反汇编文件hello.asm,如图4-8所示。

图4-8 hello.o反汇编生成hello.asm

将hello.asm与第3章的hello.s进行对照分析:

4.4.1 反汇编与汇编代码对照分析

以下选取关键代码片段进行对比:

片段1:函数入口与栈帧分配

hello.s(汇编代码):

图4-9 hello.s片段1

hello.asm(反汇编机器码):

图4-10 hello.asm片段1

对照分析:

1. 指令编码一致性:

pushq %rbp → 55(1字节操作码)。

subq $32, %rsp → 48 83 ec 20(48为64位前缀,83 ec 20表示sub $0x20)。

2. 操作数处理:

汇编代码中的栈偏移(如-20(%rbp))在机器码中转换为具体偏移(如-0x14(%rbp))。

片段2:条件分支(if (argc !=5))

hello.s(汇编代码):

图4-11 hello.s片段2

hello.o(反汇编机器码):

图4-12 hello.asm片段2

对照分析:

1. 标签地址转换:

汇编代码中的标签.L2在机器码中替换为相对偏移量0x19(0x32-0x17=0x19)。

2. 指令编码:

je .L2 → 74 19(74为je操作码,19为跳转偏移)。

片段3:函数调用(puts与exit调用)

hello.s(汇编代码):

图4-13 hello.s片段3

hello.o(反汇编机器码):

图4-14 hello.asm片段3

对照分析:

1. 符号地址占位:

call puts@PLT → e8 00 00 00 00,后4字节为占位符,由重定位条目R_X86_64_PLT32指导链接器填充实际地址。

2. 重定位修正值:

Addend = -0x4:修正时需从符号地址减去4(因指令编码中call的偏移计算方式)。

片段4:printf函数调用与参数传递

hello.s(汇编代码):

图4-15 hello.s片段4

hello.o(反汇编机器码):

图4-16 hello.asm片段4

对照分析:

1. 字符串地址加载:

.LC1在.rodata节中的偏移为+0x2c,重定位类型为R_X86_64_PC32,表示相对rip的地址计算。

2. 可变参数处理:

movl $0, %eax清零%eax,表明printf无浮点参数,与汇编代码一致。

片段5:循环控制(for (i=0; i<10; i++))

hello.s(汇编代码):

图4-17 hello.s片段5

hello.o(反汇编机器码):

图4-18 hello.asm片段5

对照分析:

跳转偏移计算:

jle .L4 → 7e a4,偏移量0xa4为补码形式,实际跳转目标为0x95 + 0xa4 + 2 = 0x3b(含指令长度修正)。

4.4.2 机器语言与汇编语言的映射特点

1. 操作数处理差异

表4-1 机器语言与汇编语言对比

对比项

汇编代码

机器码

标签地址

符号标签(如.L2)

相对偏移量(如0x19)

函数调用地址

call puts@PLT

e8 00 00 00 00 + 重定位条目

数据加载地址

lea .LC0(%rip),%rax

lea 0x0(%rip),%rax + 重定位

2. 重定位机制

(1)重定位类型:

R_X86_64_PLT32:用于函数调用,修正为PLT表项地址。

R_X86_64_PC32:用于相对地址计算(如字符串常量加载)。

(2)占位符填充:

所有外部符号地址在汇编阶段均为0,由链接器根据重定位表修正。

4.4.3 关键结论

1. 指令编码一致性:

汇编指令(如movq、call)与机器码严格对应,操作数(立即数、寄存器)直接编码。

2. 符号与重定位:

外部符号(如printf)和跨节引用(如.rodata)依赖重定位表修正。

3. 分支与跳转:

标签地址转换为相对偏移量,由汇编器自动计算。

4.5 本章小结

本章分析了汇编阶段的关键过程:

1. 机器码生成:汇编器将.s中的助记符转换为二进制指令,如push %rbp→55。

2. ELF结构:目标文件包含代码段(.text)、数据段(.rodata)及重定位表(.rela.text),为链接提供基础。

3. 重定位需求:未解析的符号(如printf)在.rela.text中记录,需链接阶段解析地址。

4. 地址占位符:跳转和函数调用的目标地址在汇编阶段暂为0,链接时填充实际地址。

通过汇编阶段,程序从人类可读的汇编代码转变为机器可执行的二进制格式,同时保留了符号和重定位信息,为最终的可执行文件生成奠定了基础。

第5章 链接

5.1 链接的概念与作用

链接(Linking)是将多个可重定位目标文件(.o)和库文件合并生成单一可执行文件的过程。其核心作用包括:

1. 符号解析(Symbol Resolution):将代码中引用的符号(如printf、exit)与其定义(在库或其它目标文件中)绑定。

2. 重定位(Relocation):合并代码段和数据段,调整符号的地址偏移,使其在最终内存布局中正确定位。

3. 地址空间分配(Address Space Allocation):为代码段(.text)、数据段(.data、.rodata)等分配虚拟地址。

在Hello程序中,链接器将hello.o与C标准库(如libc.so)链接,生成可执行文件hello。

5.2 在Ubuntu下链接的命令

使用的命令是:ld -o hello -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 /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o

图5-1 链接

如图5-1所示,hello出现在了目录下。

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

首先用readelf -a hello > hello1.elf生成hello可执行文件的elf格式hello1.elf:

图5-2 生成hello1.elf

各段信息如下:

1. ELF头

hello1.elf中的ELF头以16字节序列Magic开始,剩下的部分包含帮助链接器语法分析和解释目标文件的信息。基本信息与前一部分ELF头基本一致(如Magic,类别等),而类型发生改变,程序头大小和节头数量增加,并且获得了入口地址。

图5-3 ELF头

关键字段分析:

类型:EXEC表示可执行文件。

入口点:0x4010f0为程序起始地址,对应_start函数。

节头部表:共27个节,字符串表索引为26(.shstrtab节)。

2. 节头部表

与hello.elf相比,在链接之后的内容更加详细,记录了各个节的大小Size、偏移量Offset和程序被载入到虚拟地址的起始地址Address等基本信息。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。

图5-4 节头部表

关键节解析:

.text:主程序代码段,包含main函数和_start函数,地址0x4010f0,权限AX(可分配、可执行)。

.rodata:只读数据段,存储”Hello %s %s %s
“等字符串,地址0x402000。

.got.plt:全局偏移表的PLT部分,存储动态链接函数(如printf)的实际地址,地址0x404000。

3. 程序头表

程序头表是一个结构数组,描述如何将文件加载到内存。

图5-5 程序头表

关键段解析:

PHDR:程序头表自身的元数据,权限为只读(R)。

INTERP:指定动态链接器路径(/lib64/ld-linux-x86-64.so.2),权限为只读。

LOAD(代码段):虚拟地址0x401000,权限R E(可读、可执行),包含.init、.plt、.text等节。

LOAD(数据段):虚拟地址0x403e50,权限RW(可读写),包含.dynamic、.got.plt、.data等节。

4. 动态节

动态节记录动态链接所需信息:

图5-6 动态节

关键条目解析:

NEEDED:依赖的共享库为libc.so.6。

PLTGOT:PLT的GOT表地址为0x404000,与.got.plt节的地址一致。

5. 重定位节

重定位信息记录符号地址修正需求:

图5-7 重定位节

printf调用:位于.got.plt的偏移0x404020,类型为R_X86_64_JUMP_SLOT,表示需动态链接器填充printf的实际地址。

6. 符号表

符号表分为动态符号表(.dynsym)和静态符号表(.symtab)。

图5-8 动态符号表

图5-9 静态符号表

关键项解析:

UND符号:如printf、puts,表示需动态解析的外部函数。

main函数:地址0x401125,大小163字节,位于.text节(Ndx=15)。

5.4 hello的虚拟地址空间

在GDB中执行命令info proc mappings,获取进程的虚拟地址空间信息:

图5-10 GDB查看虚拟地址空间

与ELF程序头表对照分析如下:

1. 代码段(.text)

ELF程序头表:

图5-11 ELF代码段

GDB内存映射:

图5-12 GDB代码段

对照结论:

地址范围完全一致,权限R E对应r-xp,包含.text节(主程序代码)和.plt.sec节(PLT桩代码)。

程序入口点_start位于0x4010f0,main函数位于0x401125,均在代码段范围内。

2. 只读数据段(.rodata)

ELF程序头表:

图5-13 ELF只读数据段

GDB内存映射:

图5-14 GDB只读数据段

对照结论:

地址范围一致,权限R对应r–p,包含.rodata节(存储字符串常量)和.eh_frame节(异常处理框架)。

字符串”Hello %s %s %s
“位于.rodata节,地址0x402000 + 0x2c = 0x40202c。

3. 数据段(.data、.got.plt)

ELF程序头表:

图5-15 ELF数据段

GDB内存映射:

图5-16 GDB数据段

对照结论:

地址偏移差异:

ELF程序头表中数据段起始地址为0x403e50,而GDB中显示为0x404000。原因为内存页对齐(4KB),0x403e50向上对齐到0x404000,导致实际加载地址与文件偏移不一致。

关键节定位:

.got.plt位于0x404000,存储动态链接函数(如printf)的实际地址。

.data节位于0x404048,存储全局变量。

4. 动态链接库(libc.so.6)

ELF程序头表:

动态节(.dynamic)声明依赖库libc.so.6(NEEDED条目)。

GDB内存映射:

图5-17 GDB动态链接库

对照结论:

libc.so.6被加载到独立地址空间,代码段权限为r-xp(可读、可执行),数据段为rw-p(可读写)。

printf的实际地址(如0x7ffff7c2d1a0)在运行时由动态链接器解析并填充到.got.plt。

5. 动态链接器(ld-linux-x86-64.so.2)

GDB内存映射:

图5-18 GDB动态链接器

对照结论:

动态链接器的代码段(r-xp)负责解析外部符号(如printf),并更新.got.plt。

6. 栈(Stack)

GDB内存映射:

图5-19 GDB栈

对照结论:

栈由操作系统动态管理,权限为rw-p,用于存储局部变量和函数调用帧。

5.5 链接的重定位过程分析

首先使用objdump -d -r hello > hello1.asm命令,生成hello的反汇编文件hello1.asm,如图5-20所示。

图5-20 生成hello1.asm

接下来将hello1.asm与第四章中生成的hello.asm文件进行比较:

1. 函数调用的重定位

示例1:puts函数调用

hello.o:

图5-21 hello.asm的puts函数调用

机器码为e8 00 00 00 00(call指令占位符),重定位类型为R_X86_64_PLT32。

重定位规则:修正值为puts@PLT地址与下一条指令地址(0x28)的偏移,公式为S + A – P,其中:

S = 符号puts的实际地址(PLT条目地址)。

A = Addend(-4)。

P = 被修正的位置地址(0x24)。

hello:

图5-22 hello1.asm的puts函数调用

机器码为e8 43 ff ff ff,对应偏移0xfffff943(小端编码)。计算验证:

puts@plt地址为0x401090,下一条指令地址为0x40114d。

偏移量 = 0x401090 – 0x40114d = -0xbd,补码表示为0xfffff943,与机器码一致。

示例2:printf函数调用

hello.o:

图5-23 hello.asm的printf函数调用

hello:

图5-24 hello1.asm的printf函数调用

机器码e8 08 ff ff ff对应偏移0xfffff908。计算验证:

printf@plt地址0x4010a0,下一条指令地址0x401198。

偏移量 = 0x4010a0 – 0x401198 = -0xf8,补码0xfffff908。

关键结论:

所有动态链接函数(如puts、printf)通过R_X86_64_PLT32重定位到PLT条目,首次调用时触发动态链接器解析实际地址并更新GOT。

2. 数据引用的重定位

示例:字符串地址加载(.rodata)

hello.o:

图5-25 hello.asm的字符串地址加载

重定位类型为R_X86_64_PC32,需计算相对rip的偏移。

重定位规则:修正值为.rodata中字符串地址与下一条指令地址的偏移,公式为S + A – P。

hello:

图5-26 hello1.asm的字符串地址加载

机器码为48 8d 05 c3 0e 00 00,对应偏移0xec3。计算验证:

字符串地址0x402008,下一条指令地址0x401145。

偏移量 = 0x402008 – 0x401145 = 0xec3,与机器码一致。

关键结论:

静态数据(如字符串)通过R_X86_64_PC32重定位,链接器计算相对rip的偏移,生成可直接访问的地址。

总结链接过程的核心步骤:

(1)符号解析:解析hello.o中的未定义符号(如puts、printf),绑定到动态库libc.so.6。

(2)地址分配:合并所有目标文件的段,为代码段、数据段分配虚拟地址(如.text从0x401000开始)。

(3)重定位修正:根据重定位类型(如PLT32、PC32)计算偏移,修正指令中的占位符。

(4)生成PLT/GOT:创建PLT条目(如puts@plt)和GOT表项(如0x404018),支持动态链接。

5.6 hello的执行流程

使用GDB执行hello,首先在_start、main、exit函数设置断点,然后分别执行。

断点1:_start函数(地址:0x4010f0)

图5-27 _start函数

关键调用:call [rip+0x2edb]

目标地址:0x403ff0(GOT表中__libc_start_main的地址)。

断点2:main函数(地址:0x401125)

图5-28 main函数

main函数执行过程:

1. 参数检查:if (argc != 5):跳转到错误处理或继续执行。

2. 动态函数调用:调用printf@plt(地址:0x4010a0)和sleep@plt(地址:0x4010e0)。

3. 等待输入:调用getchar@plt(地址:0x4010b0)。

断点3:exit函数(地址:0x4010d0)

main函数返回后,__libc_start_main调用exit清理资源:

图5-29 调用exit

终止步骤:

1. 清理资源:

刷新I/O缓冲区;执行通过atexit注册的函数。

2. 系统调用终止:

exit最终调用_exit系统调用,终止进程。

图5-30 exit函数

完整调用链与地址总结:

表5-1 hello执行过程

阶段

函数/地址

作用

入口点

0x4010f0 (_start)

程序启动,调用__libc_start_main

C运行时初始化

__libc_start_main@0x403ff0

初始化环境,调用main函数

数据加载地址

lea .LC0(%rip),%rax

lea 0x0(%rip),%rax + 重定位

代码执行

0x401125 (main)

执行程序逻辑

动态函数调用

0x4010a0 (printf@plt)

0x4010e0 (sleep@plt)

动态链接到libc的printf函数

等待输入

0x4010b0(getchar@plt)

等待输入

程序终止

0x4010d0 (exit@plt)

调用exit清理资源并退出

系统调用退出

syscall 60 (_exit)

内核终止进程

5.7 Hello的动态链接分析

首先启用gdb,在main入口处设置断点,然后运行程序。此时GOT表尚未填充实际地址,查看printf@got.plt的值:

图5-31 查看运行前的printf@got.plt

0x401040指向PLT桩代码,即动态链接前的默认跳转地址。

查看动态链接后的printf@got.plt:

0xf7c606f0为printf在libc.so.6中的实际地址,动态链接器已填充该值。

图5-32 查看运行后的printf@got.plt

5.8 本章小结

链接是程序从可重定位目标文件到可执行文件的关键步骤,核心过程包括:

1. 符号解析:绑定外部符号(如printf)到动态库中的定义。

2. 重定位:修正代码和数据的地址偏移,合并各段到虚拟地址空间。

3. 动态链接:通过PLT/GOT实现延迟绑定,减少启动开销。

通过分析hello的ELF格式、虚拟地址空间及动态链接机制,揭示了链接器如何协调多模块代码,确保程序在内存中正确加载和执行。此过程体现了操作系统与编译工具链的深度协同,是计算机系统分层抽象的重要实践。

第6章 hello进程管理

6.1 进程的概念与作用

进程是操作系统进行资源分配和调度的基本单位,其核心特征包括:

1. 独立性:每个进程拥有独立的虚拟地址空间、文件描述符和运行上下文。

2. 动态性:进程具有创建、执行、暂停、终止等生命周期状态。

3. 并发性:多个进程通过时间片轮转或优先级调度共享CPU资源。

在Hello程序中,进程的作用是:

1. 通过进程地址空间隔离确保程序运行的稳定性。

2. 通过进程调度实现多任务并发执行(如同时运行Shell和Hello程序)。

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

Shell(Bash) 是用户与操作系统内核交互的接口,是一种交互型的应用级程序。基本功能是解释并运行用户的指令,它提供了一个界面,用户可以通过这界面访问操作系统内核。

Shell的处理流程如下:

1. 读取输入:解析用户输入的命令(如./hello 学号 姓名 手机号 秒数)。

2. 创建进程:通过fork()创建子进程。

3. 加载程序:在子进程中调用execve()加载Hello可执行文件。

4. 进程管理:支持后台运行(&)、作业控制(jobs、fg)和信号处理(Ctrl-C、Ctrl-Z)。

6.3 Hello的fork进程创建过程

在终端中输入命令行./hello 2023113286 Cai Fengqi 2后,shell会处理该命令。由于这里是hello,因此不属于内置命令范畴。

如果判断出不是内置命令,则会调用fork 函数创建一个新的子进程,这个子进程得到一份与父进程用户级虚拟空间相同且独立的副本——包括数据段、代码、共享库、堆和用户栈。此时,当父进程调用fork 时,子进程可以读写父进程中打开的任何文件,只有当子进程对数据做出更改时才会在内存中创建副本。

父进程与子进程之间的区别在于它们拥有不同的PID。父进程也负责子进程的回收。如果子进程结束时父进程仍然存在,那么将有父进程进行回收;反之则有init进行回收,一切进程都是init的子进程。

6.4 Hello的execve过程

execve 的功能是在当前进程的上下文中加载并运行一个新程序。具体如下:

1. 替换进程映像:将当前进程的代码段、数据段替换为hello程序的代码。

2. 参数传递:接收命令行参数(argv)和环境变量(envp)。

3. 重置状态:关闭未标记CLOEXEC的文件描述符,重置信号处理函数。

6.5 Hello的进程执行

进程执行的上下文与调度:

1. 上下文切换:

用户态:执行Hello的代码(如printf、sleep)。

内核态:处理系统调用(如write、getchar)和中断。

2. 时间片调度:

当Hello进程的时间片用完,CPU切换到其他进程。

通过/proc/sys/kernel/sched_rr_timeslice_ms查看时间片长度(默认10ms)。

用户态与内核态转换示例:

printf(“Hello World”);  // 触发write系统调用 → 切换到内核态

6.6 hello的异常与信号处理

6.6.1 异常类型与信号生成

Hello程序可能触发的异常及对应信号如下:

表6-1 异常类型

异常类型

信号

触发场景

默认处理方式

用户中断

SIGINT

按下Ctrl-C

终止进程

终端挂起

SIGTSTP

按下Ctrl-Z

暂停进程,转为后台作业

非法内存访问

SIGSEGV

访问未分配内存(如空指针解引用)

终止进程并生成核心转储

非法指令

SIGILL

执行无效机器码(如篡改程序二进制)

终止进程

子进程状态变更

SIGCHLD

子进程终止或暂停

忽略(需父进程显式处理)

6.6.2 键盘操作与信号响应

通过键盘操作触发信号,验证进程状态变化及处理逻辑:

1. 正常运行

打印10次信息,间隔的sleep秒数=手机号%5,以输入回车为标志结束程序,并回收进程。

图6-1 正常运行

2. 按下Ctrl-C(SIGINT)

进程收到 SIGINT 信号,程序立即终止。Shell提示符恢复,进程完全退出。

原理:SIGINT默认终止前台进程,操作系统回收所有资源(内存、文件描述符等)。

在ps中查询不到其PID,在job中也没有显示,可以看出Ctrl-C彻底结束hello。

图6-2 Ctrl-C

3. 按下Ctrl-Z(SIGTSTP)

键入Ctrl-Z会发送SIGTSTP信号给前台进程组的每个进程,结果是hello进程暂停,转为后台作业,状态标记为Stopped。Shell提示符恢复,可通过jobs查看作业状态。

原理:SIGTSTP默认暂停进程,进程状态保存(寄存器、内存映像),转为后台作业。

图6-3 Ctrl-Z

(1) ps:查看进程状态 & jobs:列出后台作业

使用ps命令可以查看当前所有进程以及它们的PID,进程包括bash,hello以及ps。使用jobs命令可以查看当前的作业,可以看出当前的作业是hello进程,且状态是已停止。

图6-4 ps & jobs

(2) pstree:显示进程树

图6-5 pstree

(3) fg:恢复前台执行

进程从暂停状态恢复,继续执行剩余循环(前面已循环3次,此时执行剩余的7次)。

图6-6 fg

(5) kill:终止进程

使用kill命令可以给指定进程发送信号。比如kill -9 4264是指向PID为4264的进程发送SIGKILL信号。这个命令会使进程立即终止,无法清理资源(如未保存的数据)。

当再次使用ps时可以发现hello进程已经被杀死。

图6-7 kill

4. 按下Enter

随意按回车键2次,getchar()读取到换行符(
),程序继续执行下一条指令,正常结束。同时,因为缓冲区留有未处理回车,程序结束后又多了2行回车。

图6-8 换行

5. 键盘乱按

无关输入的字符串留到缓冲区,被printf打印了出来。

如果期间输入回车,那么hello结束后,换行后输入的字符会当做Shell的命令行输入。

图6-9 乱按

6.7本章小结

本章通过分析Hello进程的创建(fork)、加载(execve)、执行和信号处理,揭示了操作系统进程管理的核心机制:

1. 进程隔离:通过虚拟地址空间和上下文切换实现资源隔离。

2. 动态调度:时间片轮转平衡CPU利用率与响应速度。

3. 信号机制:提供用户与进程交互的灵活控制方式。

实验部分结合ps、jobs、kill等命令,验证了进程状态转换和信号处理的实时效果,加深了对进程生命周期管理的理解。

第7章 hello的存储管理

7.1 hello的存储器地址空间

1. 逻辑地址(Logical Address)

逻辑地址是程序代码中直接使用的内存地址,通常由段选择符(Segment Selector)和偏移量(Offset)组成。在Intel x86架构中,逻辑地址通过段式管理转换为线性地址。

在hello的汇编代码(.s文件)中,所有内存操作均使用逻辑地址,例如:

movl $0, -4(%rbp)   # 将0存储到栈帧中偏移-4的位置(逻辑地址)

此处,-4(%rbp)是一个逻辑地址,由基址寄存器rbp和偏移量-4构成。

2. 线性地址(Linear Address)

线性地址是段式管理后的结果,是逻辑地址到物理地址转换的中间形式。在平坦模式下,逻辑地址与线性地址等价。

由于现代操作系统禁用分段机制,hello程序的逻辑地址直接映射为线性地址。例如:

addq $8, %rax       # 线性地址计算:rax = rax + 8 

此处,%rax存储的是线性地址,无需段式转换。

3. 虚拟地址(Virtual Address)

虚拟地址是进程视角的独立地址空间,每个进程拥有从0x00000000到0x7FFFFFFFFF(x86-64用户空间)的连续地址范围。虚拟地址通过页表映射到物理地址,实现内存隔离和共享。

代码段(.text):

hello的main函数入口地址为0x401125,这是链接后确定的虚拟地址。

  65: 0000000000401125   163 FUNC    GLOBAL DEFAULT   15 main 

数据段(.rodata):

字符串常量”Hello %s %s %s
“存储在虚拟地址0x402000处:

Contents of section .rodata: 

 402000 48656c6c 6f202573 20257320 25730a00  hello %s %s %s.. 

4. 物理地址(Physical Address)

物理地址是实际内存芯片中的存储单元地址,由内存管理单元(MMU)通过页表转换得到。进程无法直接访问物理地址,需由操作系统和硬件协作管理。

当hello进程执行时,其虚拟地址通过多级页表转换为物理地址。例如:

虚拟地址0x401125(main函数入口)的转换:

(1)页表查询:

CR3寄存器保存页表基地址,通过四级页表(PML4 → PDPT → PD → PT)逐级索引。

假设最终页表项指向物理页框号(PFN)0x12345。

(2)物理地址生成:

物理地址 = PFN × 页大小(4KB) + 页内偏移 = 0x12345 << 12 + 0x125 = 0x12345125。

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

逻辑地址结构:由段选择符(段号)和段内偏移量组成。段选择符指向段表项,段表中存储段基地址、段限长和权限信息。

转换公式:线性地址 = 段基地址 + 段内偏移量

Linux的优化:Linux通过将段基地址设为0(如用户代码段__USER_CS和用户数据段__USER_DS),使得逻辑地址直接等于线性地址,简化了传统段式管理的复杂性。

图7-1 段式管理

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

页表层级:

PGD(页全局目录):通过线性地址高10位索引。

PUD(页上级目录):次高10位索引。

PMD(页中间目录):再次高10位索引。

PTE(页表项):最后10位索引,指向物理页帧。

物理地址生成:最终物理地址由页帧号(PFN)和页内偏移(低12位)组成。

图7-2 页式管理

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

TLB作用:缓存最近使用的页表项(PTE),命中时直接返回物理地址,未命中时需遍历四级页表。

页表查询流程:

CR3寄存器保存PGD基地址。

线性地址分解为四部分(PGD/PUD/PMD/PTE索引),逐级查询页表项。

若某级页表不存在,触发缺页中断。

图7-3 四级页表的转换

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

1. 物理地址划分:

物理地址分为标记(Tag)、组索引(Index)和块偏移(Offset)。

2. 三级Cache查询:

L1 Cache:最快但容量最小(32KB),检查标记匹配。

L2 Cache:较大(256KB),处理L1未命中。

L3 Cache:共享缓存(16MB),最后一级缓存。

3. 访问流程:物理地址依次查询L1→L2→L3 Cache,若均未命中则访问主存(约100ns)。

图7-4 CPU三级Cache

7.6 hello进程fork时的内存映射

fork()系统调用创建子进程时,采用写时复制(Copy-On-Write)机制:

1. 内存映射过程

复制页表:子进程共享父进程的页表,所有页标记为只读。

写操作触发复制:当父/子进程尝试修改共享页时,内核分配新物理页并复制内容。

2. hello的fork场景:

Shell调用fork()创建子进程后,子进程尚未执行execve(),内存内容与Shell相同。

7.7 hello进程execve时的内存映射

execve()系统调用替换进程地址空间,加载hello程序:

1. 内存映射步骤

(1)释放旧地址空间:销毁原进程的代码段、数据段和堆栈。

(2)加载新程序段:

代码段(.text):映射到只读页,权限为R-X。

数据段(.data/.bss):映射到可读写页,权限为RW-。

(3)设置堆栈:分配栈空间,初始化argc和argv参数。

2. hello的地址空间布局:

0x400000-0x401000 : .text (代码段) 

0x403000-0x404000 : .data (全局数据) 

0x7ffffffde000-0x7ffffffff000 : 栈空间 

图7-5 映射后内存空间

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

缺页故障(Page Fault)在访问未映射或权限不足的虚拟地址时触发:

1. 缺页类型:

主要缺页:需从磁盘加载数据(如文件映射页)。

次要缺页:物理页已分配但未映射(如COW触发)。

2. 处理流程:

(1)缺页原因分类:

页未分配:访问未映射的地址(如访问未初始化的堆内存)。

权限错误:写只读页(如修改代码段)。

(2)内核处理步骤:

分配物理页,更新页表项。

若因COW触发,复制物理页并修改权限。

3. hello中的缺页场景:

首次访问动态分配的栈变量(如局部数组)时触发缺页,内核分配物理页。

图7-6 缺页中断处理

7.9动态存储分配管理

动态内存分配通过malloc和free管理堆空间,Hello中间接使用(如printf调用malloc):

1. 分配策略:

brk:扩展堆空间,适用于小内存请求(<128KB)。

mmap:映射匿名页,适用于大内存请求(≥128KB)。

2. 延迟分配:malloc返回虚拟地址,首次访问触发缺页中断分配物理页。

3. 碎片管理:伙伴系统合并空闲块,减少内存碎片。

7.10本章小结

本章详细分析了hello程序的存储管理机制:

1. 地址转换:逻辑地址通过段式、页式管理转换为物理地址,TLB和Cache优化性能。

2. 内存映射:fork利用写时复制节省内存,execve重建地址空间加载程序。

3. 缺页处理:按需分配物理页,支持动态内存扩展。

4. 动态内存:malloc实现堆空间管理,支持灵活数据存储。

通过存储管理,操作系统实现了内存隔离、资源共享和高效访问,为hello程序提供了稳定的运行环境。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

Linux操作系统通过统一的设备文件模型管理所有硬件设备,核心设计如下:

1. 设备文件与设备驱动

(1)设备文件:硬件设备在文件系统中的抽象,分为三类:

字符设备:按字节流访问(如键盘、终端),对应Hello中的stdin和stdout。

块设备:按数据块访问(如硬盘),支持随机读写。

网络设备:通过套接字接口访问(如网卡)。

(2)设备驱动:内核模块实现设备控制逻辑,通过/dev目录暴露设备接口。

2. 虚拟文件系统(VFS)

(1)作用:提供统一的文件操作接口(如read、write),屏蔽不同设备的差异。

(2)关键数据结构:

file_operations:定义设备驱动支持的操作函数指针。

inode:描述文件元数据(如设备类型、权限)。

3. IO调度与缓冲机制

块设备调度:使用CFQ(Completely Fair Queuing)等算法优化磁盘访问顺序。

页缓存(Page Cache):缓存文件数据,减少直接磁盘访问。

4. hello中的IO设备交互示例:

printf(“Hello World”);  // 写入字符设备(终端) 

getchar();              // 读取字符设备(键盘) 

8.2 简述Unix IO接口及其函数

Unix系统通过系统调用提供基础IO操作,核心函数如下:

表8-1 Unix IO

函数

作用

hello中的使用场景

open

打开文件或设备

标准输入输出已由Shell预先打开

read

从文件描述符读取数据

getchar()内部调用read(0, …)

write

向文件描述符写入数据

printf()内部调用write(1, …)

close

关闭文件描述符

程序退出时自动关闭

ioctl

设备控制(如设置终端模式)

终端窗口调整大小时触发

文件描述符(File Descriptor)

标准流映射:

0:stdin(标准输入,对应键盘)

1:stdout(标准输出,对应终端)

2:stderr(标准错误)

8.3 printf的实现分析

printf是标准库(如glibc)提供的格式化输出函数,其实现分为以下阶段:

1. 格式化字符串解析

(1)参数处理:解析%s、%d等格式符,将变量转换为字符串。

(2)可变参数列表:通过va_list宏访问不定参数(如argv[1])。

2. 缓冲区管理

(1)缓冲策略:

行缓冲(Line Buffered):终端输出时,遇到换行符
立即刷新缓冲区。

全缓冲(Fully Buffered):文件输出时,缓冲区满后刷新。

(2)缓冲区结构:

struct _IO_FILE { 

    char *buf_base;     // 缓冲区起始地址 

    char *buf_ptr;      // 当前写入位置 

    int   buf_size;     // 缓冲区大小 

}; 

3. 系统调用与终端显示

(1)write系统调用:最终通过sys_write将数据从用户态缓冲区拷贝至内核。

(2)终端驱动与显示:

内核将数据写入终端设备的环形缓冲区。

终端驱动将字符编码转换为像素信号(如通过帧缓冲区)。

显示控制器(如DRM/KMS)逐行扫描像素数据,输出到屏幕。

图8-1 printf的数据流

8.4 getchar的实现分析

getchar用于从标准输入读取单个字符,其实现依赖终端设备和内核输入处理:

1. 终端输入模式

(1)规范模式(Canonical Mode):

输入按行处理,支持编辑(退格、删除)。

按下回车键后,内核将整行数据传递给程序。

(2)非规范模式(Non-Canonical Mode):

立即返回输入的每个字符(如游戏中的实时按键响应)。

2. 键盘中断处理

(1)硬件中断:按键触发键盘控制器中断(IRQ1)。

(2)读取扫描码:内核从键盘缓冲区读取扫描码,转换为ASCII字符。

(3)终端缓冲区:字符暂存于tty结构的输入队列,等待read调用读取。

3. 系统调用与用户程序交互

(1)read系统调用:getchar最终调用read(0, &c, 1),从终端缓冲区读取字符。

(2)阻塞与非阻塞:

阻塞模式:若缓冲区为空,进程休眠等待输入(Hello中getchar()的默认行为)。

非阻塞模式:通过fcntl设置O_NONBLOCK标志,立即返回EAGAIN错误。

4. 示例:getchar执行流程

int getchar() { 

    unsigned char c; 

    if (read(0, &c, 1) == 1)  // 从文件描述符0读取1字节 

        return c; 

    return EOF; 

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章深入剖析了Hello程序的IO管理机制:

1. 设备抽象与VFS:Linux通过设备文件和统一接口屏蔽硬件差异,Hello通过printf和getchar与终端交互。

2. 系统调用与缓冲:write和read是IO的核心系统调用,标准库通过缓冲区优化性能。

3. 终端输入输出:键盘和显示器通过中断驱动与内核协作,支持行编辑和实时显示。

实验验证表明,IO管理是用户程序与硬件设备间的桥梁,其高效实现依赖于操作系统的分层设计与硬件协同。

结论

从源代码到进程终止,Hello程序完整经历了计算机系统的多层级协作,其生命周期可逐条总结如下:

1. 预处理(Preprocessing)

任务:通过宏替换、头文件展开、条件编译和注释删除,将hello.c转换为纯C代码hello.i。

系统支持:预处理器(cpp)基于文本操作实现,依赖文件系统管理头文件路径。

2. 编译(Compilation)

任务:对hello.i进行词法分析、语法分析、语义检查和中间代码优化,生成与机器无关的汇编代码hello.s。

系统支持:编译器(gcc)基于符号表管理变量作用域,利用抽象语法树(AST)实现语义逻辑的精确映射。

3. 汇编(Assembly)

任务:将hello.s中的汇编指令转换为机器码,生成可重定位目标文件hello.o,包含代码段(.text)、数据段(.rodata)和符号表。

系统支持:汇编器(as)解析指令编码,依赖ELF格式规范组织二进制内容。

4. 链接(Linking)

任务:合并hello.o与共享库(如libc.so),解析外部符号(如printf),分配虚拟地址空间,生成可执行文件hello。

系统支持:链接器(ld)通过重定位表修正地址引用,动态链接器(ld-linux)运行时加载共享库并更新GOT/PLT。

5. 进程管理(Process Management)

任务:Shell调用fork()创建子进程,execve()加载hello的代码段和数据段,CPU调度执行指令,处理信号(如SIGINT、SIGTSTP)。

系统支持:写时复制(COW)优化内存使用,进程控制块(PCB)保存上下文信息,调度器分配时间片实现并发。

6. 存储管理(Memory Management)

任务:将虚拟地址通过四级页表转换为物理地址,TLB加速地址翻译,三级Cache减少内存访问延迟,处理缺页异常动态分配物理页。

系统支持:MMU硬件实现地址转换,操作系统通过页表隔离进程地址空间,Buddy系统管理物理内存分配。

7. 输入输出管理(I/O Management)

任务:printf通过行缓冲策略调用write系统输出至终端,getchar通过read系统调用从键盘读取输入。

系统支持:字符设备驱动处理终端I/O,VFS抽象统一接口,页缓存优化文件访问性能。

8. 终止与资源回收(Termination)

任务:进程退出时释放内存、关闭文件描述符,父进程(Shell)通过wait回收退出状态码。

系统支持:内核销毁进程描述符,Slab分配器回收内核对象,文件系统更新引用计数。

通过本次实验,我的感悟如下:

hello程序虽小,却完整映射了计算机系统的核心机制。从文本替换到物理信号,每一层抽象既是技术的结晶,亦是妥协的艺术。未来的系统设计需在性能、安全与易用性间寻求平衡,而深入理解现有机制,正是创新的起点。

附件

文件名称

功能

hello.c

源程序

hello.i

预处理后得到的文本文件

hello.s

编译后得到的汇编语言文件

hello.o

汇编后得到的可重定位目标文件

hello.elf

用readelf读取hello.o得到的ELF格式信息

hello.asm

反汇编hello.o得到的反汇编文件

hello

链接后得到的可执行文件

hello1.elf

用readelf读取hello得到的ELF格式信息

hello1.asm

反汇编hello得到的反汇编文件

参考文献

[1]  Randal E.Bryant David R.O'Hallaron.深入理解计算机系统(第三版).机械工业出版社,2016.

[2]  段页式访存——逻辑地址到线性地址的转换[EB/OL]. (2020-4-21)[2020-4-21]. https://blog.csdn.net/Pipcie/article/details/105670156

[3]  分页存储 — 地址变换机构[EB/OL]. (2016-6-19)[2016-6-19]. https://blog.csdn.net/dongyanxia1000/article/details/51711780

[4]  深入了解GOT,PLT和动态链接[EB/OL]. (2018-4-9)[2018-4-9]. https://www.cnblogs.com/pannengzhi/p/2018-04-09-about-got-plt.html

[5]  九、操作系统——基本地址变换机构(详解)[EB/OL]. (2020-5-26)[2020-5-26]. https://blog.csdn.net/weixin_44827418/article/details/106352038

[6]  段式、页式内存管理以及linux采用的方案图解[EB/OL]. (2020-7-3)[2020-7-3]. https://blog.csdn.net/jinking01/article/details/107098437

[7]  【ARM-MMU】ARMv8-A 的4K页表四级转换(VA -> PA)的过程[EB/OL]. (2019-3-1)[2019-3-1]. https://blog.csdn.net/liujingyu_1205/article/details/88062712

[8]  CPU三级缓存技术解析[EB/OL]. (2022-1-25)[2022-1-25].  https://zhuanlan.zhihu.com/p/461548456

[9]  操作系统—(35)缺页中断与缺页中断处理过程[EB/OL]. (2020-6-9)[2020-6-9].  https://blog.csdn.net/qq_43101637/article/details/106646554

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容