摘 要
本研究以hello程序为研究载体,聚焦其从源码到运行终止的完整生命周期,系统探究计算机系统的底层工作机制与核心设计逻辑。研究基于Ubuntu操作系统与GCC工具链,通过预处理、编译、汇编、链接等关键步骤,结合GDB调试、反汇编分析、ELF文件解析等技术手段,深入剖析了程序转化过程中各中间文件(hello.i、hello.s、hello.o 等)的格式与作用,厘清了从高级语言到机器指令的转化路径。在此基础上,进一步研究了程序运行阶段的进程管理(fork、execve 创建与加载)、存储管理(段页式地址转换、TLB与Cache加速)、I/O管理(Unix I/O接口、printf/getchar底层实现)等核心机制,揭示了动态链接(PLT/GOT延迟绑定)、写时复制(COW)、缺页中断等系统优化策略的工作原理。研究成果验证了一切皆文件的I/O设备抽象模型与用户态、内核态的协同工作模式,为深入理解计算机系统的整体架构与运行逻辑提供了实践支撑。
****关键词:****计算机系统;编译链接;进程管理;存储管理;
目 录
[++++第1章 概述++++](#第1章 概述)
[++++1.1 Hello简介++++](#1.1 Hello简介)
[++++1.2 环境与工具++++](#1.2 环境与工具)
[++++1.3 中间结果++++](#1.3 中间结果)
[++++1.4 本章小结++++](#1.4 本章小结)
[++++第2章 预处理++++](#第2章 预处理)
[++++2.1 预处理的概念与作用++++](#2.1 预处理的概念与作用)
[++++2.3 Hello的预处理结果解析++++](#2.3 Hello的预处理结果解析)
[++++2.4 本章小结++++](#2.4 本章小结)
[++++第3章 编译++++](#第3章 编译)
[++++3.1 编译的概念与作用++++](#3.1 编译的概念与作用)
[++++3.2 在Ubuntu下编译的命令++++](#3.2 在Ubuntu下编译的命令)
[++++3.3 Hello的编译结果解析++++](#3.3 Hello的编译结果解析)
[++++3.4 本章小结++++](#3.4 本章小结)
[++++第4章 汇编++++](#第4章 汇编)
[++++4.1 汇编的概念与作用++++](#4.1 汇编的概念与作用)
[++++4.2 在Ubuntu下汇编的命令++++](#4.2 在Ubuntu下汇编的命令)
[++++4.3 可重定位目标elf格式++++](#4.3 可重定位目标elf格式)
[++++4.4 Hello.o的结果解析++++](#4.4 Hello.o的结果解析)
[++++4.5 本章小结++++](#4.5 本章小结)
[++++第5章 链接++++](#第5章 链接)
[++++5.1 链接的概念与作用++++](#5.1 链接的概念与作用)
[++++5.2 在Ubuntu下链接的命令++++](#5.2 在Ubuntu下链接的命令)
[++++5.3 可执行目标文件hello的格式++++](#5.3 可执行目标文件hello的格式)
[++++5.4 hello的虚拟地址空间++++](#5.4 hello的虚拟地址空间)
[++++5.5 链接的重定位过程分析++++](#5.5 链接的重定位过程分析)
[++++5.6 hello的执行流程++++](#5.6 hello的执行流程)
[++++5.7 Hello的动态链接分析++++](#5.7 Hello的动态链接分析)
[++++5.8 本章小结++++](#5.8 本章小结)
[++++第6章 hello进程管理++++](#第6章 hello进程管理)
[++++6.1 进程的概念与作用++++](#6.1 进程的概念与作用)
[++++6.2 简述壳Shell-bash的作用与处理流程++++](#6.2 简述壳Shell-bash的作用与处理流程)
[++++6.3 Hello的fork进程创建过程++++](#6.3 Hello的fork进程创建过程)
[++++6.4 Hello的execve过程++++](#6.4 Hello的execve过程)
[++++6.5 Hello的进程执行++++](#6.5 Hello的进程执行)
[++++6.6 hello的异常与信号处理++++](#6.6 hello的异常与信号处理)
[++++第7章 hello的存储管理++++](#第7章 hello的存储管理)
[++++7.1 hello的存储器地址空间++++](#7.1 hello的存储器地址空间)
[++++7.2 Intel逻辑地址到线性地址的变换-段式管理++++](#7.2 Intel逻辑地址到线性地址的变换-段式管理)
[++++7.3 Hello的线性地址到物理地址的变换-页式管理++++](#7.3 Hello的线性地址到物理地址的变换-页式管理)
[++++7.4 TLB与四级页表支持下的VA到PA的变换++++](#7.4 TLB与四级页表支持下的VA到PA的变换)
[++++7.5 三级Cache支持下的物理内存访问++++](#7.5 三级Cache支持下的物理内存访问)
[++++7.6 hello进程fork时的内存映射++++](#7.6 hello进程fork时的内存映射)
[++++7.7 hello进程execve时的内存映射++++](#7.7 hello进程execve时的内存映射)
[++++7.8 缺页故障与缺页中断处理++++](#7.8 缺页故障与缺页中断处理)
[++++第8章 hello的IO管理++++](#第8章 hello的IO管理)
[++++8.1 Linux的IO设备管理方法++++](#8.1 Linux的IO设备管理方法)
[++++8.2 简述Unix IO接口及其函数++++](#8.2 简述Unix IO接口及其函数)
[++++8.3 printf的实现分析++++](#8.3 printf的实现分析)
[++++8.4 getchar的实现分析++++](#8.4 getchar的实现分析)
第1章 概述
1.1 Hello简介
Hello 程序的"P2P"本质是源码到可执行文件的点对点转化流程------以 hello.c 源码为起点,通过预处理、编译、汇编、链接四步流水线式转化,将高级语言代码逐步映射为机器可执行的指令序列,实现源码文件到可执行文件的点对点精准转化。而"O2O"则是用户操作到硬件交互的线上线下闭环------线上用户通过终端输入运行命令与参数,触发 Shell 创建进程、加载程序;线下程序通过系统调用与硬件交互,将格式化信息输出到显示器(printf),同时捕获键盘输入(getchar),完成用户指令-系统调度-硬件响应的全链路交互。整个过程贯穿了计算机系统的分层抽象、资源调度与IO交互核心机制,是理解系统工作原理的典型载体
1.2 环境与工具
1.软硬件环境:
·硬件环境:x86-64 架构计算机、内存16GB;256GHD Disk
·软件环境:Ubuntu 20.04 LTS 操作系统、GCC 9.4.0编译工具链、GNU C 标准库(libc.so.6)。
2.工具:
·编译工具:GCC
·调试工具:GDB
·分析工具:readelf、objdump;
·辅助工具:bash终端、ls、cat
1.3 中间结果
|------------|-----------------------------|
| 文件名 | 功能 |
| hello.i | 预处理后得到的源代码文件 |
| hello.s | 编译后得到的汇编语言文件 |
| hello.o | 汇编后得到的可重定位目标文件 |
| hello.elf | 用readelf读取hello.o得到的ELF格式信息 |
| hello.asm | 反汇编hello.o得到的反汇编文件 |
| hello | 可执行ELF文件 |
| hello1.elf | 用readelf读取hello得到的ELF格式信息 |
| hello1.asm | 反汇编hello得到的反汇编文件 |
1.4 本章小结
本章明确了Hello程序的核心流程定位(P2P转化+O2O交互),详细列出了实验所需的软硬件环境与开发调试工具,梳理了程序转化过程中关键中间文件的名称及作用。通过本章内容,建立了对Hello程序生命周期的整体认知,为后续深入分析预处理、编译、汇编、链接等转化环节,以及进程管理、存储管理、I/O交互等运行机制奠定了基础。
第2章 预处理
2.1 预处理的概念与作用
1.概念:是指预处理器在程序运行前,对源文件进行简单加工的过程,由预处理器完成。其工作是扫描源代码,识别以#开头的预处理指令,并执行对应的操作,最终生成一个经过处理的、不含预处理指令的纯源代码文件,供后续编译阶段使用
2.作用:核心价值是简化代码编写、提高代码复用性、增强代码灵活性和可移植性
(1)用于宏定义和宏函数,预处理器会将代码中所有宏名替换为对应的文本
(2)将指定头文件的内容完整插入到当前源文件中,包含标准库文件和自定义头文件,实现代码复用
(3)根据条件(#ifdef / #ifndef / #if /#endif)决定哪些代码会被保留到预处理后的文件中,哪些会被剔除
(4)用于取消已定义的宏(#undef),之后可以重新定义该宏
(5)辅助指令:如#error强制触发编译错误,并输出指定提示信息,用于检查编译条件;#pragma向编译器传递特定指令
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析 
hello.i查看结果如上图,发现除了#部分被扩充后,程序的其他部分不变。在main函数前是stdio.h、unistd.h、stdlib.h的依次展开,第3044行# 10 "hello.c" 2是预处理的行标记,意思是从下一行开始的代码,来自hello.c文件的第 10 行。
以stdlib.h的展开为例:#include把指定的头文件内容添加到源文件中,包含了atoi()、exit()、malloc()、free()、mbtowcs()等通用工具函数;同时插入附带__attribute__(如__nothrow__)等 GCC 扩展属性(用于编译器优化)和EXIT_SUCCESS等辅助宏定义
预处理器遇到#include<stdlib.h>时,会在系统的头文件路径下查找stdlib.h文件,然后将其复制到源文件中,不会对头文件中的内容做任何修改。
2.4 本章小结
本章首先介绍了预处理的概念、作用,以及在Ubuntu下预处理的命令。然后实现了在Linux系统下用命令对hello.c程序进行预处理,得到了hello.i,并对预处理后的程序进行了分析:头文件的展开、原代码的位置标记和源代码
第3章 编译
3.1 编译的概念与作用
1.概念:编译是把高级编程语言代码,通过专门的工具(编译器/Compiler)转换成汇编语言的过程
2.作用:使高级语言转化为汇编语言,让机器能够读懂指令,提高程序运行效率、检查代码语法错误、类型错误、逻辑不合法等问题以及优化代码和提高可移植性
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s


3.3 Hello的编译结果解析
3.3.1 初始部分

(1).file "hello.c" 记录这段汇编对应的源文件名是 hello.c
(2).text 切换到代码段
(3).section .rodata 定义并切换到只读数据段
(4).align 8 要求后面的数据按8 字节对齐
(5).string "\347\224\250\346\263\225: Hello \345\255\246... " 定义一个字符串常量,存到.rodata段中
(6).string "Hello %s %s %s\n" 定义字符串常量"Hello %s %s %s\n",存到.rodata段(是后续printf的格式控制串)
(7).text 再次切换回代码段
(8).globl main 告诉链接器main是全局可见的符号
(9).type main, @function 指定main是一个函数
3.3.2 数据
(1)常量

.LC0和.LC1是字符串常量(存放在只读数据段.rodata),对应 C 里的"参数错误提示"和"Hello %s %s %s\n"

由此可知,当argc不等于5时,把.LC0的地址给%rdi,然后调用put函数打印

.LC1首地址也存在%rdi处
(2)参数

(3)局部变量

函数的唯一局部变量是i,存在栈的-4(%rbp)处,初始化为0
(4)类型
命令movl(操作4字节)对应 C 的int类型(比如argc、i是 int);movq(操作 8字节)对应C的指针类型(比如argv是char**,每个元素是 8 字节指针)
3.3.3 赋值操作

用mov指令进行赋值,对应c里面的int i = 0
3.3.4 算术操作

add指令用于加法操作,对应for循环的每次循环结束后i++

同理,寄存器%rax处的地址加24,对应argv + 3实现指针偏移
3.3.5 关系操作

cmp比较立即数5和argc的大小,根据条件码,当argc不等于5时跳转到.L2

同理,对应循环中i<=9的判断,成立时跳转
3.3.6 控制转移指令

判断argc是否等于5,如果等于5执行if语句,不等于5执行else语句,对应if(argc!=5)语句

for循环中先初始化i=0,然后无条件跳转到循环条件判断,i<=9时执行循环体代码,每次执行后自增
3.3.7 函数操作
(1)main函数
①参数传递:
对应参数int argc,char*argv[]
②函数调用:使用call指令调用函数,并且将调用函数的地址存入栈中。调用的函数包括printf、exit、sleep
③局部变量:函数内部定义局部变量i用于for循环
④函数返回:
设置返回值为0并用ret进行函数返回
(2)printf函数
①参数传递:将参数.L1,argv[1], argv[2], argv[3]传给printf
②函数调用:call指令调用函数,对应C的printf("Hello %s %s %s\n", argv[1], argv[2], argv[3])
(3)put函数

①参数传递:将.L0传给put
②函数调用:call指令调用puts函数,打印错误提示
(4)exit函数

①参数传递:把退出码1传给%edi
②函数调用:call调用exit,程序退出
(5)atoi函数

①参数传递:先令argv + 32字节 = argv[4],然后将argv[4]传给atoi
②函数调用:call调用atoi,将argv[4]转化为整数
(6)sleep函数

①参数传递:%edi = 转换后的整数,传给sleep
②函数调用:call调用sleep,休眠对应秒数
(7)getchar函数

直接call调用,用来暂停程序
3.4 本章小结
本章介绍了编译的概念和作用,然后介绍了编译指令,在Ubuntu下实现了把hello.i文件转换成hello.s文件,最后通过解析hello.s文件,探究了C语言中的数据处理,赋值、算术、关系等操作,控制跳转和类型转换,以及函数调用方面的内容
第4章 汇编
4.1 汇编的概念与作用
1.概念:是指利用汇编器(as)将汇编语言翻译成机器语言,并把机器语言指令打包为一个可重定位目标文件文件,生成目标文件(.o)文件的过程,该文件是一个二进制文件
2.作用:将汇编语言转化为机器可以直接识别执行的机器语言,能够直接操作寄存器和访存,便于程序调试和优化
4.2 在Ubuntu下汇编的命令
命令:gcc -c hello.s -o hello.o

4.3 可重定位目标elf格式
首先输入命令readelf -a hello.o > hello.elf获得hello.o的ELF格式,然后进入文本编辑器查看该文件。

(1)ELF头

ELF头开头是Magic,系统通过读取Magic判断该文件是否是合法的ELF文件,此外ELF头还记录了文件的基本属性:
|---------|-------------------------------|-------------------------------------------------|
| 类别 | ELF64 | 这是64位ELF文件 |
| 数据 | 小端序 | 数据的字节序是小端(默认字节序) |
| 类型 | REL(可重定位文件) | 该文件是目标文件(.o) |
| 系统架构 | Advanced Micro Devices X86-64 | 目标 CPU 架构是 x86-64(AMD/Intel 64 位处理器) |
| 入口点地址 | 0x0 | 可重定位文件没有入口点(入口点是可执行文件的属性,.o 文件是片段)。 |
| 节头表起始位置 | 1264(bytes into file) | 节头表在文件中的偏移量是 1264 字节 |
| 节头数量 | 14 | 该文件包含14个节(section)(如.text、.rodata、.rela.text 等) |
(2)节头

节头记录了ELF文件中每个节(section)的信息,如代码节.text,只读数据节.rodata,重定位表节.rela.text,符号表节.symtab,字符串表节.strtab,每个节都有自己的类型和地址、偏移
(3)重定位项目

因为hello.o是可重定位文件,其.text节中的外部符号(puts、exit等)的地址还未确定,所以重定位表节.rela.text记录这些需要重定位的信息,链接时链接器根据符号表找到符号的真实地址并填充。需要重定位的信息如上图所示:
(4)符号表

符号表.symtab节记录了ELF文件中的符号表,该符号表包含一个条目的数组,存放一个程序定义和引用的全局变量及函数的信息(不包含局部变量),其中type为类型,bind为绑定属性,vis为可见性
4.4 Hello.o的结果解析
首先输入命令objdump -d -r hello.o > hello.asm获得hello.o的反汇编文件,hello.asm文件内容如图所示:


与hello.s相比:
(1)增加了机器语言
每一条指令前增加了一组可变长度的字节序列,即该指令的机器语言,二者是一一对应的关系。机器语言由操作码(标识指令的功能)和操作数(指令的操作对象)组成
(2)操作数不同


hello.s中的操作数为十进制,反汇编中为十六进制(0x开头),二者只是进制不同,但数值相同
(3)分支跳转


相比于hello.s中的用符号标签跳转(.L2),反汇编中采用主函数+段内偏移量的方式进行跳转
(4)函数调用


hello.s中直接采用函数名的方式进行函数调用。而反汇编中用重定位占位符(机器码中的00 00 00 00)+重定位项(R_X86_64_PLT32 puts-0x4)进行调用(因为puts是外部函数,地址未确定)
4.5 本章小结
本章首先介绍了汇编的含义和功能,然后介绍如何在Ubuntu下用指令将hello.s转换为hello.o目标文件,接着生成ELF格式的文件hello.elf,解析该文件内容,依次分析文件中的每个节,最后反汇编生成hello.o文件的反汇编文件,并将其与汇编语言文件hello.s对比,分析它们的相同点和不同点。
第 5 章 链接
5.1 链接的概念与作用
1.概念:分为静态链接和动态链接两种核心类型,由链接器(Linker)完成。连接是将多个目标文件、以及程序依赖的库文件(静态库 .a/.lib、动态库 .so/.dll)进行整合,解析所有符号引用,生成可直接运行的可执行文件的过程
2.作用:
(1)符号解析:解决不同目标文件之间的函数、变量调用依赖
(2)地址重定位:为全局变量、函数分配最终的内存地址,确保程序运行时指令能正确访问目标内存
(3)库复用:静态链接将库代码复制到可执行文件中,程序运行不依赖外部库,体积大但兼容性强;动态链接在程序运行时才加载库文件,可执行文件体积小,多个程序可共享同一个库,节省内存
5.2 在Ubuntu下链接的命令
命令:ld -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 -o hello


5.3 可执行目标文件hello的格式
首先输入命令readelf -a hello.o > hello.elf获得hello.o的ELF格式,然后进入文本编辑器查看该文件
(1)ELF头

hello1.elf的ELF头也是以16字节序列的Magic开始,其他为该文件的基本信息:类别、数据、版本、OS、ABI版本、类型(可执行文件)、入口点地址、程序头起点、节头部表、节头的大小等
(2)节头

节头描述一个节的名称、类型、大小、权限、在文件中的位置和偏移量等,连接器链接时会将各个相同段合并,且重新设置各个符号的地址
(3)程序头

每个程序头描述一个段的类型、内存权限、内存地址,系统加载程序时,按LOAD 段的vaddr(内存地址)、offset(文件偏移)、flags(权限),将文件内容映射到内存,比如把.text所在的 LOAD 段映射为"可读可执行",.data所在的 LOAD段映射为"可读可写"
(4)段节

一个段由多个逻辑节合并而成,权限相同的节会被合并到同一个LOAD段,非 LOAD段仅存储元信息,不单独占用内存区域,其内容包含在LOAD段中。
(5)Dynamic Section

动态链接节对应.dynamic节的内容,存储动态链接的核心元信息,包括程序运行时依赖的动态库、其他各个节的地址(存储符号字符串、动态符号表、哈希表等),动态链接器加载时,读取Dynamic Section,找到依赖的libc.so.6,解析符号表,完成puts/printf等函数的地址绑定
(6)重定位节

对应.rela.plt节,是动态链接时需要重定位的信息的节,动态链接器运行时,将libc.so.6中puts/printf的实际内存地址,写入.got.plt的对应偏移量位置,让程序能正确调用这些库函数
(7)符号节


符号节记录符号序号、内存地址、类型、名称等信息,动态链接器通过符号表找到puts/printf的名称,在libc.so.6中匹配对应的函数地址,完成符号绑定
5.4 hello的虚拟地址空间
首先输入命令./hello,然后输入start,程序停在main函数开头,最后执行info proc mappings命令,GDB会显示进程的内存映射表,对应虚拟地址空间各段,结果如下图:

内存映射中objfile = /home/benjamin/bigwork/hello的条目,是hello程序自身的虚拟地址段(由ELF程序头的LOAD段加载到内存形成);每个虚拟地址段对应一个LOAD 段,每个LOAD段又包含多个 ELF节。
虚拟地址段0x400000 ~ 0x401000对应第一个LOAD段(权限只读),这个段是hello的只读辅助信息段,存储动态链接器路径(.interp)、符号哈希表(.hash)等链接的元信息
虚拟地址段0x401000 ~ 0x402000对应第二个LOAD段(权限可读可执行),这是hello的代码段,存储main函数的可执行指令(.text)、程序初始化和终止代码(.init/.fini)、动态函数跳转表(.plt);先前ELF头的入口点地址是0x4010f0,正好落在这个段内,符合代码段的定义
虚拟地址段0x402000 ~ 0x403000对应第三个LOAD段(权限只读),这是hello的只读数据段,存储字符串常量
虚拟地址段0x403000 ~ 0x404000对应第四个LOAD代码段(权限只读),这是hello的重定位信息段,存储动态链接时需要修正的地址表,权限只读是因为重定位信息是静态的,运行时由动态链接器读取,无需修改
内存映射中objfile = /usr/lib/x86_64-linux-gnu/libc-2.31.so的条目,是hello依赖的C标准库的虚拟地址段,这些段是libc.so自身的LOAD段加载到内存的结果
5.5 链接的重定位过程分析
首先输入命令objdump -d -r hello > hello1.asm生成反汇编文件hello1.asm;

5.5.1.hello1.asm相比hello.asm的不同之处
(1)函数数量增加
因为链接器将共享库中hello.c用到的函数加入可执行文件中,所以链接后的hello1.asm中多出了_init、.plt、puts@plt、printf@plt、getchar@plt、atoi@plt、exit@plt、sleep@plt、_start等函数的代码


(2)地址变化
相比于hello.asm中以相对于节起始的偏移作为地址的方法,hello1.asm中的地址为固定的虚拟地址,因为链接器在链接时,会按照ELF的地址布局规则(默认从0x400000开始),为.text/.data等节分配运行时虚拟地址,把目标文件的相对偏移转换为绝对虚拟地址
(3)函数调用方式不同

hello.o的反汇编程序中采用main+偏移的方式调用其他函数

而hello的反汇编程序中直接指向.plt节的puts@plt条目,因为在链接过程中,链接器解析了重定位条目,call之后的字节代码被链接器直接修改为目标地址与下一条指令的地址之差
(4)跳转指令参数不同


在链接过程中,链接器解析了重定位条目,并计算相对距离,修改了对应位置相应函数与下条指令的相对地址,从而得到新的跳转地址
(5)指令完整性不同


链接器读取hello.o的重定位表,为每个外部符号调用计算并填充实际偏移,将e8 00 00 00 00的占位偏移,替换为计算后的实际偏移,完成重定位
5.5.2 链接过程
(1)符号解析:读取hello.o的符号表,识别出puts、printf是未定义符号(UND)
,然后从libc.so.6(C 标准库)中找到puts、printf的符号定义,并标记为动态链接符号
(2)节合并与地址分配:先将hello.o的.text节,与启动文件(crt1.o)的.text节、.init/.fini节合并,形成hello的.text节;然后为合并后的节分配虚拟地址(如.text从0x401000开始,.plt从0x401020开始),让hello.o的相对偏移映射为hello的 绝对地址
(3)重定位:见5.5.3
(4)生成动态链接结构:为puts/printf创建PLT跳转条目和GOT地址表;记录依赖库(libc.so.6)、符号表位置等信息,供动态链接器运行时使用
5.5.3 重定位过程
(1)重定位节和符号定义:链接器将所有相同类型的节合并为同一类型的节,然后将运行时的内存地址赋给新的节,赋给输入模块定义的每个节和输入模块定义的每个符号。此时程序中每条指令和全局变量都有唯一的运行内存地址
(2)重定位节中的符号引用:根据重定位条目,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。
(3)地址计算:以hello.o中callq puts的重定位项为例
①链接器为puts创建PLT条目,即puts@plt被分配到0x401090;
②计算callq指令的相对偏移:偏移=目标地址(0x401090)-指令下一条地址(0x40114a),即 0x401090 - 0x40114a= 0xffffff46(小端序为46 ff ff ff);
③将hello.o中e8 00 00 00 00的占位偏移,最终指令变为e8 46 ff ff ff callq 401090 <puts@plt>。
5.6 hello的执行流程
在终端先使用指令gdb ./hello进入调试,然后输入info file查看程序入口

之后在程序入口(即_start)处设置断点,启动程序,之后用si指令单步执行,直到遇到call指令

继续执行si,然后跳转到__libc_start_main里,重复上述步骤记录依次经过的函数。















经过的过程:
(1)启动:_start、__libc_start_main、_init、__libc_csu_init
(2)执行:main、puts、printf、getchar、atoi
(3)退出:_exit、__libc_csu_fini、_fini
依次经过的函数及其地址:
_start 0x4010f0
__libc_start_main 0x7ffff7de2fc0
(__GI___cxa_atexit 0x7ffff7e05e10
__libc_csu_init 0x4011d0
_init 0x40100
_setjmp 0x7ffff7e01cb0
__sigsetjmp 0x7ffff7e01be0
__sigjmp_save 0x7ffff7e01c60)
main 0x401125
puts 0x401090
exit 0x4010d0
(printf 0x4010a0
atoi 0x4010c0
sleep 0x4010e0
getchar 0x4010b0
__libc_csu_fini 0x401240
_fini 0x401248)
exit后面的函数其实未执行
5.7 Hello的动态链接分析
动态链接的核心思想是将程序的代码和数据拆分为主程序与共享库,主程序编译时仅记录对共享库中函数、数据的符号,而非直接嵌入共享库的代码;调用共享库函数时编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。所以连接器为该引用生成一条重定位记录,然后在程序加载的时候再解析它。动态链接实现延迟绑定的关键机制就靠GOT(全局偏移表)和PLT(过程链接表):
·PLT:.plt/.plt.sec节,存储跳转指令,初始时指向动态链接器的解析逻辑;
·GOT:.got/.got.plt节,存储外部函数的实际地址,动态链接前指向PLT的跳板代码,动态链接后被填充为libc中函数的真实地址。
由ELF文件中的节头可知,GOT基地址为0x404000

(1)动态链接前:
程序加载后、首次调用puts前,GOT表项未被填充,PLT 仅存储跳转指令:

通过puts@plt首地址0x401090以及jmpq *0x2f7d(%rip)可知puts@plt的GOT表项地址是0x404018

然后运行到_start断点处,此时动态链接未执行,查看GOT表项0x404018的初始值为0x401030,这个值指向puts@plt的后续指令
(2)动态链接后

在puts@plt处设置断点,continue运行到此处,之后si单步执行直到进入动态链接器的解析逻辑,然后c一次,让动态链接器完成地址解析

此时查看GOT表项0x404018的内容,发现被填充为libc中puts的真实地址0x7ffff7e43450,查看该地址对应的符号,可以确认是libc提供的puts函数
5.8 本章小结
本章首先介绍了链接的概念和作用,然后展示了在Ubuntu下如何使用指令生成可执行文件hello,并解析了hello的ELF格式下的内容,接着利用gdb观察了hello的虚拟地址空间使用情况以及分析了hello的重定位过程和执行过程,最后分析了hello的动态链接过程。
第 6 章 hello进程管理
6.1 进程的概念与作用
1.概念:一个执行中程序的实例。进程是操作系统进行资源分配和调度的基本单位,本质是程序在计算机中的一次执行过程
2.作用:进程提供给应用程序两个关键抽象:
(1)逻辑控制流 (Logical control flow):每个程序似乎独占地使用CPU(2)私有地址空间 (Private address space):每个程序似乎独占地使用内存系统
进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中
6.2 简述壳Shell-bash的作用与处理流程
1.作用:shell 是一个交互型应用级程序,代表用户运行其他程序;是信号处理的代表,负责各进程创建与程序加载运行及前后台控制,作业调用,信号发送与管理等
2.处理流程:
(1)读取输入:通过readline库读取你在终端输入的命令,并保存到输入缓冲区
(2)解析输入:把命令拆分、处理特殊字符(检查是否有别名、通配符(*)、重定向(>)、管道(|)等)、语法检查
(3)路径查找:判断命令是内置命令还是外部命令,内部命令直接执行,外部命令先查找路径再执行
(4)执行命令:
内置命令:Shell 直接调用自身的函数执行(无需创建新进程);
外部命令:Shell 通过fork()创建子进程,再通过exec()将子进程的代码段替换为/bin/命令,由子进程执行。
(5)处理输出:把命令执行结果(标准输出stdout)打印到终端,若有错误,则把错误信息(标准错误stderr)输出到终端(或重定向到文件)
(6)回收子程序:
前台命令:Shell 会阻塞,等待子进程执行完毕,通过waitpid()回收子进程,避免僵尸进程;
后台命令(加&):Shell不阻塞,继续接收新命令,子进程在后台运行,结束后 Shell 自动回收
(7)返回状态码:命令执行完毕后,Shell 会把退出状态码保存到特殊变量?中,? = 0表示命令执行成功,$? ≠ 0表示命令执行失败(如1= 普通错误,2= 语法错误)
6.3 Hello的fork进程创建过程
命令:./hello 2024112143 马梓彤 15708576634 1
过程:当bash作为父进程执行外部命令./hello时,会先触发fork()系统调用,内核随即为新的子进程分配唯一PID,该子进程得到与父进程用户级虚拟地址空间相同的一份副本,包括代码和数据段、堆、共享库以及用户栈,fork()一次调用会产生两次返回------父进程获取子进程的PID,子进程则返回 0
6.4 Hello的execve过程
当子进程触发execve("./hello", ["./hello"], 环境变量)系统调用时,内核首先校验hello文件的合法性;随后销毁子进程当前所有的内存空间(包括从父进程继承的代码段、数据段、堆、栈等),解析hello的ELF文件头,将其代码段、数据段加载到子进程的虚拟地址空间,初始化子进程的栈和堆并设置程序执行入口点(对应hello的main函数起始地址);整个过程中子进程的核心属性(PID、打开的终端文件描述符、当前工作目录、环境变量等)均保持不变,若加载成功,内核调度子进程开始执行hello的指令直至其调用exit 退出,若加载失败,子进程会返回错误码并立即退出,最终由父进程bash通过waitpid回收其资源。与fork一次调用返回两次不同,execve调用一次并不返回
6.5 Hello的进程执行
1.进程就绪:
在bash子进程完成execve加载hello后,内核会将该进程标记为就绪态,并完成核心初始化:进程上下文初始化包括内核在该进程的进程控制块中记录关键上下文信息------CPU寄存器、虚拟内存映射(hello的代码段/数据段地址)、打开的文件描述符、程序计数器 PC 指向 hello 的 main 函数入口地址;此时调度器入队,该进程被加入CPU就绪队列,等待调度器分配时间片
此时进程处于内核态,内核完成初始化后,触发用户态切换:将保存的初始上下文加载到CPU寄存器,PC指向hello的main函数第一条指令,CPU 从内核态切回用户态,hello进程进入待执行状态
2.进程调度与首次执行:
(1)调度器选择:内核调度器根据进程优先级(普通进程默认相同)、等待时间等,从就绪队列中选中hello进程
(2)上下文切换:若当前CPU正在运行其他进程,内核会先保存当前进程的上下文,再加载hello的上下文(从hello的PCB读取寄存器、PC等值到CPU)
(3)时间片启动:调度器为 hello分配时间片,并启动时钟中断计时器,hello 进程进入运行态,开始执行用户态指令(逻辑控制流启动)
- 用户态执行与内核态切换:
hello 的核心逻辑会触发多次用户态到内核态转换,且可能被时间片中断,包括的操作有:
(1)用户态执行核心指令:CPU按PC指向的地址逐条执行hello 的用户态指令,此时进程处于用户态,仅能访问自身虚拟内存,无内核资源直接操作权限
(2)系统调用触发内核态转换:当执行到printf时,其底层会调用write函数的系统调用------CPU执行 int 0x80或syscall 指令,触发陷阱,保存当前 hello 的用户态上下文到PCB,切换为内核态
(3)内核态处理write逻辑:首先校验文件描述符(1对应 stdout);然后将输出数据从hello的用户态内存拷贝到内核缓冲区;最后调用终端驱动程序,将数据写入终端设备。完成后,内核更新hello的上下文(PC指向write返回后的指令),触发用户态切换,恢复hello的执行
(4)时间片中断:当hello 执行指令较多,在时间片耗尽前,时钟中断会触发:
①CPU立即切到内核态,保存hello当前的上下文(PC指向未执行完的指令),将其状态从运行态切回就绪态,重新入队
②调度器选择下一个进程,加载其上下文,切回用户态执行;
③当hello再次被调度时,内核恢复其保存的上下文,PC 回到中断时的位置,继续执行未完成的指令
4.进程退出与资源回收
hello 执行完 main 函数的 return 0 后,由于return会隐式调用exit(0),所以CPU切到内核态,保存hello最终的上下文;然后清理资源:关闭hello打开的所有文件描述符、回收hello的虚拟内存空间(代码段、数据段、堆、栈)、将hello 的进程状态标记为终止态,并向父进程发送SIGCHLD信号;接着bash原本阻塞在 waitpid() 系统调用(内核态),收到信号后,内核唤醒shell,读取 hello PCB 中的退出码,销毁 hello 的 PCB(上下文彻底清除),完成资源回收;最后内核调度器选择新的就绪进程,加载其上下文,切回用户态,shell恢复命令行交互,hello 的逻辑控制流彻底终止
6.6 hello的异常与信号处理
1.异常分类及处理:
|----|-----------|-----|-------|------------------|
| 类别 | 原因 | 同步性 | 可恢复性 | 例子 |
| 故障 | 潜在可恢复的错误 | 同步 | 可恢复 | 缺页、保护故障、浮点异常 |
| 陷阱 | 有意的异常 | 同步 | 执行后返回 | 系统调用 |
| 终止 | 不可恢复的错误 | 同步 | 不可恢复 | 非法指令、奇偶校验错误、机器检查 |
| 中断 | 来自I/O设备信号 | 异步 | 处理后恢复 | 时钟中断、I/O设备中断 |
中断处理:

陷阱处理:

故障处理:

终止处理:

2.信号及处理
(1)正常运行:

在程序正常运行时,打印10次提示信息,输入回车,向shell发送SIGCHLD信号,并回收进程
(2)按下回车

按下回车并不会向hello进程发送信号,只是终端向程序的标准输入写入一个换行符的普通输入操作
(3)按下Ctrl-Z

Shell进程收到SIGSTP信号,执行默认动作:从运行态切换为暂停态,暂停当前执行
(4)按下Ctrl-C

Shell进程收到SIGINT信号,Shell结束并回收hello进程
(5)运行ps指令

运行ps指令可以查看当前hello进程的状态
(6)运行jobs指令

运行jobs指令,查看当前shell的作业列表,发现进程hello被挂起
(7)运行pstree指令

输入pstree指令可以读取进程间的父子关系数据
(8)运行fg指令

发送SIGCONT(进程继续信号)信号,Shell首先打印hello的命令行命令,hello进程再从挂起处继续运行,打印剩下的语句,正常结束
(9)运行kill指令

执行kill -9 %2指令,发送SIGKILL信号,hello无法捕获该信号,内核强制终止进程并回收资源
(10)乱按

乱按26个英文字母,发现输入的字母hello 程序的输出混在一起,这些字母会暂存在终端输入缓冲区,但hello进程完全不会读取、也不会处理它们,进程的执行逻辑不受任何影响
6.7本章小结
本章首先阐述了进程的概念与作用;然后进一步分析了Shell-bash的作用与处理流程;接着用实例,介绍hello的fork进程创建过程、execve过程;最后详细分析了hello的进程执行过程,包括异常的情况和处理,以及执行时出现的信号和处理
第 7 章 hello的存储管理
7.1 hello的存储器地址空间
1.逻辑地址:是程序在编译和链接阶段由编译器、链接器生成的地址,逻辑地址是由一个段标识符加上一个指定段内相对地址的偏移量,仅对程序自身的段结构有意义,不直接对应物理内存位置
2.线性地址:是逻辑地址经CPU分段机制转换后得到的地址,现代操作系统常采用平坦模型简化分段,此时逻辑地址可直接作为线性地址。比如 hello 程序运行时,若代码段基地址被设为0x555555554000,main 函数逻辑地址(偏移)为 0x1145,则线性地址为两者之和,它是分段机制处理后的中间地址
3.虚拟地址:是程序运行时CPU直接使用的地址,对应进程独立的虚拟地址空间,经过地址翻译后得到物理地址。hello程序运行中,printf 函数的指令地址、变量i的存储地址、argv参数指向的字符串地址等,均为虚拟地址,进程通过虚拟地址访问内存,与物理内存隔离,由操作系统统一管理
4.物理地址:是实际物理内存(RAM)中的真实存储地址,由CPU的内存管理单元(MMU)通过分页机制从虚拟地址转换而来。该地址对用户程序不可见,由操作系统和硬件协同管理
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理是指把一个程序分成若干个段进行存储,每个段都是一个逻辑实体;段式管理是通过段表进行的,包括段名、段起点、装入位、段的长度等;程序通过分段划分为多个块,如代码段、数据段、共享段等。
逻辑地址到线性地址的变换如下图所示。
一个48位的逻辑地址被划分为两部分,一部分是16位段选择符,第3~15 位(共13位)是索引,用于在GDT/LDT中定位具体的段描述符(13位索引对应最大8K个描述符);第2位是TI 位(Table Indicator),表是指示位 (0表示查找全局描述符表(GDT),1 表示查找局部描述符表(LDT));第0~1位是RPL(Request Privilege Level),请求特权级,用于权限检查。另一部分是32位段内偏移量表示指令或数据在对应段内的相对位置。
第二步为查找段描述符,由段选择符的TI位和索引共同决定找哪个表、找表中哪个描述符。若TI=0,CPU会用GDT首地址+索引×8计算出目标段描述符的地址,从 GDT 中取出被选中的段描述符;若TI=1:CPU会用LDT 首地址+索引 ×8计算地址,从LDT中取出被选中的段描述符。段描述符是8字节的核心结构,图中的32 位段基地址是该段在线性地址空间的起始位置。
第三步为计算线性地址。先从选中的段描述符中提取32 位段基地址,然后将 32位段基地址与32 位段内偏移量相加,得到最终的32位线性地址。

7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。VM系统将虚拟内存分割,称为虚拟页,类似地,物理内存也被分割成物理页。
利用页表来管理虚拟页,页表就是一个页表条目(PTE)的数组,每个PTE由一个有效位和一个物理页号(PPN)组成,有效位表明了该虚拟页当前是否被缓存在DRAM中,如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置,如果发生缺页,则从磁盘读取。
页表基址寄存器(PTBR)是CPU中的专用寄存器,存储当前进程页表的物理起始地址。操作系统会为每个进程分配专属页表,进程切换时,PTBR 会同步更新为对应进程的页表物理地址,保证进程地址空间隔离。
以hello中main函数某条指令的虚拟地址0x555555555145为例,转换流程如下:
(1)PTBR定位hello进程的页表物理地址:hello进程运行时,CPU的PTBR 寄存器中存储的是hello专属页表的物理起始地址,MMU(内存管理单元)可以通过PTBR找到hello的页表在哪里;
(2)拆分hello的虚拟地址为VPN和VPO:对虚拟地址0x555555555145(以 4KB页为例,页大小= 2¹²=4096,对应低12位为偏移),虚拟页偏移量(VPO)为低12位0x145(对应图中p-1~0位),代表这条指令在虚拟页内的第325个字节;虚拟页号(VPN)高位部分0x555555555(对应图中 n1~p 位),用于索引 hello 的页表;
(3)用VPN索引页表,检查有效位:MMU 用VPN作为索引,从 PTBR 指向的页表中找到对应的页表项,若有效位= 0(缺页),说明这个虚拟页还没加载到物理内存,会触发缺页异常------ 操作系统会暂停 hello 的执行,从硬盘读取该虚拟页对应的代码到物理内存的空闲页框(比如物理页框号0x8000),然后更新该页表项,将有效位设为1,物理页号(PPN)填0x8000,之后重新执行这次地址转换;若有效位= 1,说明该虚拟页已加载到物理内存,直接从页表项中取出物理页号(PPN)
(4)拼接PPN 和VPO(PPO)得到物理地址:物理页偏移量(PPO)和虚拟页偏移量(VPO)完全相同(都是0x145),因为页内偏移是相对位置,无需转换。最终物理地址=物理页号(PPN)×页大小+物理页偏移量(PPO),代入 hello 的例子:0x8000×0x1000(4KB)+0x145 = 0x8000000+0x145=0x8000145。此时 MMU 就完成了转换,CPU 能通过0x8000145这个物理地址,从物理内存中读取 hello 程序的这条指令

7.4 TLB与四级页表支持下的VA到PA的变换
Core i7架构下的四级页表层次结构以48位虚拟地址(VA)的分段索引为核心逻辑,虚拟地址VA传送给MMU,MMU使用VPN高位作为TLBT和TLBI,向TLB中寻找匹配。如果命中,则得到物理地址PA。如果TLB中没有命中,MMU查询页表,先将虚拟地址拆分为4段各9位的虚拟页号(VPN1~VPN4)和12位的虚拟页偏移量(VPO),以CR3寄存器存储的一级页表(页全局目录,L1 PT)物理地址为遍历起点,MMU依次通过VPN1索引一级页表获取二级页表(页上级目录,L2 PT)的物理地址、VPN2索引二级页表获取三级页表(页中间目录,L3 PT)的物理地址、VPN3索引三级页表获取四级页表(页表,L4 PT)的物理地址,最后用VPN4索引四级页表,从对应的页表项(PTE)中提取物理页号(PPN),该层级结构通过分级索引的方式适配大虚拟地址空间管理需求,同时可按需创建页表项以节省物理内存,且与TLB 缓存协同减少频繁遍历页表的开销。相应的工作原理如下图:


7.5 三级Cache支持下的物理内存访问
如图为单级缓存的存储器组织结构:
每个物理地址被拆分为三部分:t 位标记(tag)、s 位组索引(set index)、b 位块偏移(block offset);缓存整体划分为 S=2^s 个组,每组包含 E=2^e 行(缓存行),每行由有效位(V)(标记数据是否有效)、标记位(tag)(匹配物理地址的 tag 段)、数据块(B=2^b 字节) 组成;缓存读的核心逻辑是先定位组,然后检查组内行的有效位+标记匹配,最后通过块偏移提取数据
三级 Cache 遵循由上至下、逐级查询、命中即返回的逻辑,L1最靠近CPU(时延最低、容量最小),L2次之,L3为多核共享缓存(容量最大、时延高于 L1/L2),整体流程如下:
(1)L1 Cache 优先查询:CPU 将虚拟地址转换后的物理地址拆分为 tag、组索引、块偏移,按图中逻辑查询 L1 Cache------用组索引定位 L1 目标组,遍历组内缓存行,若某行有效位为1且 tag与物理地址 tag 匹配(L1 命中),直接通过块偏移从数据块取数返回 CPU;若未命中,进入L2查询。
(2)L2 Cache 查询:复用图中缓存读逻辑(仅行数量、地址位宽适配L2规格),用物理地址的组索引定位 L2目标组,检查标记与有效位;若 L2 命中,提取数据返回 CPU,同时将该数据填充到 L1 Cache;若未命中,进入 L3 查询。
(3)L3 Cache 查询:L3 作为最后一级片上缓存,仍按组索引→标记匹配→有效位检查逻辑查询;若 L3 命中,提取数据返回 CPU,并逐级回写填充到 L2、L1 Cache;若 L3 未命中,触发主存访问。
(4)主存访问与缓存填充:CPU 向物理内存读取包含目标数据的缓存块(大小与 Cache 行一致),将该块依次写入 L3、L2、L1 Cache,再从 L1 Cache 通过块偏移提取目标数据返回,完成整个访问流程

7.6 hello进程fork时的内存映射
hello 进程调用fork()创建子进程时,操作系统通过写时复制(COW)机制优化内存映射:初期父子进程拥有独立但地址完全相同的虚拟地址空间,所有虚拟页均映射到同一份物理内存页,这些物理页被标记为 COW 属性且设为只读权限,无需立即复制物理页,大幅节省内存开销。
当父子进程任一方向共享的 COW 物理页执行写操作时,会触发写保护异常,内核会为该进程拷贝对应物理页生成私有副本,更新其页表使虚拟页映射到新的私有物理页(权限改为可写),另一进程仍映射原物理页以保障内存隔离。因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射
hello进程调用execve函数加载并运行新的hello可执行程序时,核心是替换原有进程地址空间的内存映射,按图中逻辑完成全新的内存区域构建:首先execve会删除当前进程已存在的所有用户态内存区域(包括原有的代码、数据、堆、栈、共享库映射区等),清空旧的内存映射关系;随后创建全新的内存区域结构,对应图中分层的内存布局:代码段(.text)和已初始化数据段(.data)从hello可执行文件映射而来,标记为私有、写时复制;未初始化数据段(.bss)、运行时堆、用户栈均为私有且请求二进制零(内核按需分配并初始化为 0),栈初始长度为 0,堆则随malloc动态扩展;共享库(如libc.so)的.text/.data 段被映射到共享库内存映射区域,由动态链接器完成共享对象到进程共享区域的映射。
最后execve会设置程序计数器(PC)指向hello代码段的入口点,同时 Linux 采用按需换入策略,仅当进程访问对应内存页时,才将hello的代码和数据页从磁盘换入物理内存,完成整个程序的内存映射构建,使进程从新程序的入口开始执行。

7.8 缺页故障与缺页中断处理
缺页故障的触发与中断处理是虚拟内存按需加载的核心机制,流程围绕 MMU 对页表项(PTE)的检查和内核异常处理展开:
(1)处理器将虚拟地址发送给 MMU (2-3)MMU 使用内存中的页表生成PTE地址(4)有效位为零, 因此 MMU 触发缺页异常(5)缺页处理程序确定物理内存中牺牲页 (若页面被修改,则换出到磁盘)(6)缺页处理程序调入新的页面,并更新内存中的PTE(7)缺页处理程序返回到原来进程,再次执行缺页的指令

7.9动态存储分配管理
1.基本方法:动态内存管理的核心是在程序运行时按需申请、使用、释放内存:
(1)核心接口:通过malloc(申请指定大小内存)、calloc(申请并初始化为 0)、realloc(调整已分配内存大小)向堆区申请虚拟内存,free释放不再使用的内存;printf内部会因可变参数处理、缓冲区动态扩容等场景调用malloc申请内存。
(2)底层管理:内存分配器维护堆区的空闲内存块链表,申请时从空闲链表中匹配合适块,释放时将块归还给链表,同时完成地址映射(虚拟地址到物理地址),依赖操作系统的页式内存管理机制
2.核心策略:
(1)分配策略:
·首次适配(First Fit):遍历空闲链表,选取第一个足够大的块(malloc默认策略,简单高效);
·最佳适配(Best Fit):选取最小的足够大的块(减少内存浪费但遍历成本高);
·下次适配(Next Fit):从上次分配位置遍历(降低遍历开销,碎片略多)。
(2)碎片管理:
·内部碎片:通过内存对齐(如 8/16 字节对齐)平衡分配效率与碎片(如申请 10 字节实际分配 16 字节);
·外部碎片:释放内存时合并相邻空闲块(coalescing),或用内存池预分配固定大小块,缓解碎片问题。
(3)性能优化:
·内存池:预分配一批固定大小的内存块,高频调用(如printf反复申请小内存)时直接从池取 / 归,减少系统调用;
·分层缓存:分配器缓存常用大小的块,避免频繁向操作系统申请(brk/sbrk或mmap)。
(4)回收策略:C语言需显式调用free回收,未回收会导致内存泄漏;释放时会检查相邻空闲块并合并,提升后续大内存申请的成功率。
7.10本章小结
本章依次介绍了hello的存储器地址空间、Intel的段式管理和逻辑地址到线性地址的变换流程、hello的页式管理和线性地址到物理地址的变换流程,以intel Core i7为例,介绍了虚拟地址VA到物理地址PA的转换、物理内存访问,分析了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理和动态存储分配管理。
第 8 章 hello的IO管理
8.1 Linux的IO设备管理方法
1.设备的模型化:Linux将所有 I/O 设备抽象为文件,纳入文件系统的管理体系,所有设备在 /dev 目录下存在对应的设备文件(特殊文件),用户通过操作该文件节点间接操作硬件,且设备文件无实际数据内容,仅包含设备号(主设备号标识设备驱动类型,次设备号区分同类型的不同设备实例)。另一方面,Linux 将设备分为三类,均以文件形式呈现:
|--------------|-----------------------------|--------------|
| 设备类型 | 抽象特性 | 典型设备 |
| 字符设备 | 按字节流顺序访问,无缓冲区,需直接与硬件交互 | 串口、键盘 |
| 块设备 | 按固定大小数据块访问,有缓冲区,支持随机读写 | 硬盘、U 盘 |
| 网络设备 | 面向数据包传输,无 /dev 节点,通过套接字接口访问 | 网卡 |
2.设备管理:Linux为所有设备文件提供统一的Unix I/O系统调用接口,用户程序无需区分设备类型,通过标准文件操作函数即可完成 I/O 操作,核心接口如下:
|--------------|-----------------|-----------------|
| 系统调用 | 功能 | 设备操作场景 |
| open() | 打开设备文件,获取文件描述符 | 初始化设备,申请硬件资源 |
| read() | 从设备读取数据到用户空间 | 从键盘读取输入、从硬盘读取数据 |
| write() | 将用户空间数据写入设备 | 向串口发送数据、向硬盘写入文件 |
| ioctl() | 设备控制接口,执行设备专属命令 | 配置串口波特率、获取硬盘参数 |
| close() | 关闭设备文件,释放资源 | 释放硬件占用,终止设备操作 |
8.2 简述Unix IO接口及其函数
Unix I/O接口是一套标准化的系统调用集合,核心设计目标是通过统一接口屏蔽文件、字符设备、块设备等不同I/O对象的差异,用户程序无需修改即可适配各类I/O场景,相应的函数如下:
|-----------------------------------------------------|-----------------------------------|
| 函数 | 功能 |
| int open(const char *path, int flags, mode_t mode) | 打开、创建 I/O 对象(文件、设备文件),返回文件描述符(fd) |
| ssize_t read(int fd, void *buf, size_t count) | 从fd对应的I/O对象读取数据到用户空间缓冲区 |
| int close(int fd) | 关闭I/O对象,释放文件描述符及关联资源 |
| int ioctl(int fd, unsigned long request,...) | 设备专属控制命令接口 |
| int fcntl(int fd, int cmd) | 文件描述符控制(修改打开模式、复制 fd等) |
8.3 printf的实现分析
printf 的完整显示链路是一套从用户态文本格式化到硬件层像素输出的跨态流程,核心是将抽象的文本信息转化为显示器可识别的像素信号。在用户态阶段,printf 首先通过 vsprintf 解析格式化字符串与可变参数,将不同类型的参数(如数字、字符串)按指定格式转换为ASCII 字符串并存储到缓冲区,随后调用 write函数触发系统调用(32 位系统中常用 int 0x80 软中断,64 位系统则用 syscall 指令),这一操作完成用户态到内核态的切换,内核的sys_write 会将缓冲区中的 ASCII 字符串传递给终端对应的字符显示驱动。
进入内核态后,字符显示驱动子程序承担起文本转像素的核心职责:驱动先将每个ASCII码匹配字模库中的点阵数据(如 8x16 点阵形式的字符轮廓),再根据字符在屏幕上的坐标位置,将点阵数据转化为对应像素的 RGB 颜色值(点亮的像素写入前景色,熄灭的写入背景色),并直接写入显存------VRAM 作为存储屏幕所有像素 RGB 信息的专用内存,已被映射到内核地址空间,驱动可直接对其进行读写操作。
最终到硬件层,显示芯片会按照固定的刷新频率逐行扫描 VRAM 中的 RGB 数据,将这些颜色信息通过 HDMI、DP 等信号线传输给液晶显示器,显示器接收信号后控制对应像素的液晶分子透光率,从而将原本的 ASCII 文本以可视化的字符形式呈现到屏幕上。整个过程既体现了用户态与内核态的权限隔离,也完成了从抽象文本到物理像素的层层转化。
https://www.cnblogs.com/pianist/p/3315801.html
8.4 getchar的实现分析
getchar的实现本质是将键盘的物理按键操作转化为用户态可读取的ASCII 字符。当用户按下键盘按键时,键盘硬件会触发异步的键盘中断,内核会立即响应并调用键盘中断处理子程序:该子程序首先读取键盘控制器发送的按键扫描码(硬件层面区分不同按键的唯一编码),再通过预设的映射表将扫描码转换为对应的ASCII码,随后将转换后的ASCII码按顺序存入系统维护的键盘缓冲区(一段环形内存区域,用于暂存未被读取的按键输入),完成从硬件信号到系统级数据的转化。
用户态调用getchar函数时,其底层会触发针对标准输入(stdin,文件描述符为 0)的read系统调用,完成用户态到内核态的切换。内核的sys_read处理逻辑会先检查键盘缓冲区:若缓冲区为空,当前进程会进入阻塞状态,等待新的键盘中断填充数据;若缓冲区有数据,则逐个读取其中的ASCII 码,且只有当读取到回车键对应的ASCII码时,才会终止读取并将此前的字符序列返回给用户态的 getchar函数,此时getchar完成一次有效输入的读取并返回目标字符。
键盘输入是异步发生的硬件事件,由中断机制保证输入被及时捕获和暂存;而getchar的读取操作是同步的,进程会按需阻塞等待输入,直到满足回车终止的条件。这种设计既保证了键盘输入的实时性,又符合用户态程序对输入读取的同步使用习惯,同时通过内核态的键盘缓冲区和系统调用层的隔离,让用户态程序无需直接操作硬件,既提升了安全性,也保证了跨硬件的兼容性。
8.5本章小结
本章首先从设备的模型化和设备管理两方面介绍了Linux的IO设备管理方法,然后简述了Unix IO接口及其常用函数,接着从用户态文本格式化到硬件层像素输出分析了printf的实现,最后按照异步硬件中断捕获和同步系统调用读取的流程分析了getchar的实现。
结论
hello所经历的过程:
1.预处理:hello.c经预处理器展开头文件、替换宏、处理条件编译,生成纯源代码文件 hello.i;
2.编译:hello.i被编译器转换为汇编语言文件 hello.s,完成语法检查、代码优化与高级语言到汇编的转换;
3.汇编:hello.s经汇编器生成可重定位目标文件 hello.o,包含机器指令、ELF 结构及未解析的外部符号;
4.链接:链接器整合hello.o与依赖库,完成符号解析和地址重定位,生成可执行ELF文件hello;
5.进程创建:shell通过fork创建子进程,继承父进程虚拟地址空间副本,子进程返回0,父进程获取子进程 PID;
6.程序加载:子进程通过execve销毁原有内存空间,加载hello的代码段、数据段,初始化栈堆并设置执行入口;
7.进程调度:内核调度器选中hello进程,切换上下文分配时间片,进程从就绪态转为运行态执行用户态指令;
8.地址转换:逻辑地址经段式管理转为线性地址,再通过页式管理、TLB缓存转换为物理地址,Cache加速内存访问;
9.IO 操作:printf调用write触发系统调用,内核驱动将ASCII码转字模写入 VRAM,显示芯片扫描输出;getchar通过read读取键盘中断存入的缓冲区数据;
10.进程终止:hello执行完 main后隐式调用exit,内核回收虚拟内存、文件描述符等资源,shell通过waitpid回收子进程。
感悟:
计算机系统的设计与实现,本质是用分而治之的智慧化解复杂问题的艺术。分层模块化是核心骨架,从预处理、编译、汇编、链接的纵向流程,到进程管理、存储管理、IO管理的横向拆分,每一层仅聚焦特定职责,既降低了单个模块的复杂度,又让系统具备独立迭代和问题定位的灵活性;而抽象则是屏蔽底层细节的关键,从一切皆文件的IO设备抽象,到虚拟内存对物理内存的抽象,再到进程对 CPU和内存资源的抽象,让上层使用者无需关注硬件细节即可高效开发,同时保障了系统的可移植性;隔离与协同的平衡是系统安全高效的基石,用户态与内核态的权限隔离避免了非法操作,进程地址空间的隔离保障了多任务并发的稳定性;"按需分配" 的设计思想贯穿始终,写时复制、缺页中断、动态链接的延迟绑定等机制,最大化提升了内存、磁盘等资源的利用率,避免了不必要的浪费。这些设计背后,是效率与安全、简洁与复杂、兼容性与创新性的持续权衡,而正是这种严谨的设计逻辑,让计算机系统能够稳定支撑从简单程序到复杂应用的各类场景,成为高效可靠的计算载体。
附件
|------------|-----------------------------|
| 文件名 | 功能 |
| hello.c | 源程序 |
| hello.i | 预处理后得到的源代码文件 |
| hello.s | 编译后得到的汇编语言文件 |
| hello.o | 汇编后得到的可重定位目标文件 |
| hello.elf | 用readelf读取hello.o得到的ELF格式信息 |
| hello.asm | 反汇编hello.o得到的反汇编文件 |
| hello | 可执行ELF文件 |
| hello1.elf | 用readelf读取hello得到的ELF格式信息 |
| hello1.asm | 反汇编hello得到的反汇编文件 |
参考文献
1\] 林来兴. 空间控制技术\[M\]. 北京:中国宇航出版社,1992:25-42. \[2\] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集\[C\]. 北京:中国科学出版社,1999. \[3\] 赵耀东. 新时代的工业工程师\[M/OL\]. 台北:天下文化出版社,1998 \[1998-09-26\]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5). \[4\] 谌颖. 空间交会控制理论与方法研究\[D\]. 哈尔滨:哈尔滨工业大学,1992:8-13. \[5\] KANAMORI H. Shaking Without Quaking\[J\]. Science,1998,279(5359):2063-2064. \[6\] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era\[J/OL\]. Science,1998,281:331-332\[1998-09-23\]. http://www.sciencemag.org/cgi/ collection/anatmorp.

