关注

程序人生-Hello’s P2P

摘  要

本报告以经典C程序“hello.c”为载体,系统地分析了从源代码到进程执行的全过程。通过预处理、编译、汇编、链接等步骤,展示了程序如何从文本文件转换为可执行目标文件;结合Linux环境下的进程管理、存储管理和I/O管理机制,详细阐述了Hello程序如何被操作系统加载、调度、执行并最终终止。报告使用GCC、readelf、objdump、gdb等工具对中间结果进行分析,并结合计算机系统层次结构,揭示了程序在硬件与操作系统协同下的完整生命周期。

关键词:预处理;编译;链接;进程管理;虚拟内存;ELF格式;系统调用

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

 “P2P”(From Program to Process)过程:

Program:程序员编写hello.c源代码,经过预处理、编译、汇编、链接生成可执行目标文件hello。

Process:在Shell中执行./hello时,操作系统通过fork()创建子进程,再通过execve()加载hello到进程地址空间,从而形成一个活跃的进程。

 “O2O”(From Zero-0 to Zero-0):

从无到有:从源代码到进程执行。

从有到无:进程执行结束后被操作系统回收,所有资源释放。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

硬件环境:x86-64架构CPU,8GB RAM

操作系统:Ubuntu 20.04 LTS

开发与调试工具:

GCC 9.3.0(编译链)

readelf、objdump(ELF文件分析)

gdb(调试与执行跟踪)

edb(动态调试)

Git(版本管理)

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

hello.i

预处理后的文本文件

hello.s

汇编语言文件

hello.o

可重定位目标文件

hello

可执行目标文件

1.4 本章小结

本章介绍了Hello程序的P2P与O2O生命周期,列出了实验环境与中间文件,为后续章节的深入分析奠定基础。

第2章 预处理

2.1 预处理的概念与作用

以下格式自行编排,编辑时删除

预处理是编译的第一步,主要处理源代码中以#开头的指令,如:

(1)展开#include包含的头文件内容。

(2)处理#define宏定义。

(3)条件编译(#ifdef、#if等)。

(4)输出为一个纯C代码文本文件(.i),不含任何预处理指令。

2.2在Ubuntu下预处理的命令

图2.1 Ubuntu下预处理命令与结果

结果:生成hello.i文件

2.3 Hello的预处理结果解析

打开hello.i可见:

头文件stdio.h等被完整展开(约上千行)。

原始的hello.c代码位于文件末尾。

所有注释被删除,宏被替换。

图2.2 hello.i内容

2.4 本章小结

预处理将多个源文件与头文件合并为一个完整的文本文件,为编译阶段做好准备。

第3章 编译

3.1 编译的概念与作用

预处理将多个源文件与头文件合并为一个完整的文本文件,为编译阶段做好准备。

注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

3.2 在Ubuntu下编译的命令

图3.1 Ubutu下编译的命令

结果:生成hello.s文件

3.3 Hello的编译结果解析

3.3.1 数据:常量、变量、类型

(1)字符串常量

图3.2 对字符串的处理

两个字符串常量存储在.rodata(只读数据段);

.LC0:错误提示字符串,包含中文字符(UTF-8编码显示为八进制转义序列);

.LC1:正常输出格式字符串。

(2)局部变量

图3.3 对局部变量的处理

argc:存储在-20(%rbp),4字节整型;

argv:存储在-32(%rbp),8字节指针;

i:存储在-4(%rbp),4字节整型。

3.3.2 赋值操作

赋初值

图3.4 对赋初值的处理

使用movl指令进行32位整型赋值;

立即数$0赋值给局部变量i。

3.3.3 类型转换

    隐式类型转换(指针到整数)

图3.5 对隐式类型转换的处理

argv[4](char*)作为参数传递给atoi;

atoi返回int存储在%eax;

int值直接传递给sleep函数。

3.3.4 sizeof操作

代码中没有显式sizeof,但编译器隐式使用:

栈分配:subq $32, %rsp,为所有局部变量分配32字节;

指针运算:addq $8, %rax,8字节对应64位系统指针大小。

3.3.5 算术操作

    (1)加法

图3.6 对加法的处理

(2)自增

图3.7 对自增的处理

3.3.6 逻辑操作

逻辑非

图3.8 对逻辑非的处理

3.3.7 关系操作

    相等判断

                   

                          图3.9 对相等判断的操作

小于等于判断

       图3.10 对小于等于判断的操作

3.3.8 数组/指针操作

    (1)数组访问

                   

                          图3.11 对数组访问的操作

基地址:argv存储在-32(%rbp);

偏移计算:addq $8, %rax,每次偏移8字节(64位指针);

解引用:movq (%rax), %rax获取数组元素。

(2)地址操作

图3.12 对地址的操作

3.3.9 控制转移

(1)if/else结构

图3.13 对if/else分支的操作

(2)for循环操作

图3.14 对for循环的操作

3.3.10 对函数操作

(1)函数调用与参数传递

图3.15函数调用

(2)函数返回

图3.16 对main函数返回的操作

返回过程:

设置返回值:movl $0, %eax,将0放入返回寄存器;

清理栈帧;

返回:ret指令从栈中弹出返回地址并跳转。

3.4 本章小结

编译器将C代码转换为汇编代码,处理了控制流、函数调用、数据访问等,为汇编阶段生成机器指令做准备。

第4章 汇编

4.1 汇编的概念与作用

    汇编器(as)将汇编代码.s转换为机器指令(二进制),并生成可重定位目标文件.o

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

4.2 在Ubuntu下汇编的命令

          图4.1 Ubuntu下汇编的命令及结果

4.3 可重定位目标elf格式

 

图4.2 hello.o的ELF格式

4.3.1 ELF头基本信息

hello.o为ELF64格式的可重定位文件,采用小端序,针对x86-64架构。文件入口点为0(不可执行),包含14个节,无程序头表。

主要节分析

(1)代码与数据节

.text节:157字节,包含main函数机器码,地址0x0(待重定位),标志AX(可分配、可执行)

.rodata节:64字节,存储两个字符串常量,8字节对齐,标志A(只读、可分配)

.data/.bss节:大小均为0,无全局变量

(2)关键元数据节

.rela.text:8个重定位条目,用于修正.text中的外部引用

.symtab:18个符号条目,包含main函数和6个未定义库函数符号

.strtab:存储符号名称字符串

.eh_frame:56字节,异常处理信息

4.3.2重定位项目分析

.rela.text节包含8个重定位条目:

(1)本地引用(2个):

.rodata - 4:偏移0x1c,类型R_X86_64_PC32,指向错误提示字符串

.rodata + 0x2c:偏移0x5f,指向"Hello %s %s %s\n"字符串

(2)库函数引用(6个):

puts - 4:偏移0x21,对应call puts@PLT

exit - 4:偏移0x2b,对应call exit@PLT

printf - 4:偏移0x69,对应call printf@PLT

atoi - 4:偏移0x7c,对应call atoi@PLT

sleep - 4:偏移0x83,对应call sleep@PLT

getchar - 4:偏移0x92,对应call getchar@PLT

重定位类型说明:

R_X86_64_PC32:PC相对寻址,用于本地数据访问

R_X86_64_PLT32:PLT跳转,用于动态库函数调用

4.3.3符号表关键条目

(1)已定义符号:

main:FUNC类型,157字节,在.text节中,全局可见

(2)未定义符号(UND):

puts, exit, printf, atoi, sleep, getchar:C库函数

_GLOBAL_OFFSET_TABLE_:全局偏移表引用

4.3.4文件布局特点

(1)地址未绑定:所有节起始地址均为0,需链接时确定

(2)外部依赖:包含6个库函数的外部引用

(3)重定位信息完整:提供代码中所有地址引用的修正信息

(4)无执行信息:缺少程序头表和入口地址,不能直接运行

4.3.5与程序关联

.text节的157字节对应main函数汇编代码;

.rodata节的64字节存储程序中的中英文字符串;

8个重定位条目对应程序中6个函数调用和2个字符串引用;

符号表中的main为唯一已定义的全局符号。

4.4 Hello.o的结果解析

图4.3 hello.o反汇编的结果

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

(1)函数开头部分

汇编指令与机器码一一对应,例如push %rbp对应机器码55;

立即数表示:汇编中的十进制数在机器码中转换为十六进制,如$32变为$0x20;

偏移量表示:-20(%rbp)在机器码中为-0x14(%rbp),两者等价(20的十六进制为0x14)。

(2)条件判断与跳转
    条件跳转指令je的机器码为74,后跟8位偏移量0x16;

在汇编代码中使用标签.L2,在机器码中转换为绝对地址0x2f;

偏移量计算:目标地址0x2f - 下条指令地址0x19 = 0x16。

(3)字符串地址加载与函数调用

lea指令的机器码中包含4个字节的占位符00 00 00 00,等待重定位;

重定位条目R_X86_64_PC32 .rodata-0x4指示如何修正该地址;

call指令同样使用占位符,重定位条目为R_X86_64_PLT32 puts-0x4。

(4)循环结构

同一节内的跳转已由汇编器计算完成;

jmp .L3转换为jmp 8b,偏移量0x53 = 0x8b - 0x38;

jle .L4转换为jle 38,偏移量0xa7(有符号数-89)= 0x38 - 0x91。

(5)printf函数参数准备

汇编指令与机器码完全对应,只是地址表示方式不同;

最后的lea指令包含占位符,重定位条目指向.rodata+0x2c(第二个字符串)。

(6)其他函数调用

所有外部函数调用都使用占位符00 00 00 00;

每个call指令都有对应的重定位条目,类型为R_X86_64_PLT32。

4.4.2 机器语言的构成与映射关系

(1)机器语言的基本构成

机器语言由操作码(opcode)和操作数(operand)组成:

操作码:指示执行什么操作(如55表示push %rbp);

操作数:提供操作所需的数据或地址。

(2)与汇编语言的主要差异

a.地址表示方式不同:

汇编语言:使用符号标签(如.L2、.LC0);

机器语言:使用绝对或相对地址(如0x2f、0x0(%rip))。

b.外部引用处理不同:

汇编语言:使用完整符号(如puts@PLT);

机器语言:使用占位符(00 00 00 00)+ 重定位信息。

c.立即数表示不同:

汇编语言:十进制或符号(如$32、$5);

机器语言:十六进制(如$0x20、$0x5)。

(3)分支转移的地址处理

a.条件跳转

汇编语言:je .L2

机器语言:74 16

汇编器计算.L2与下条指令的距离,生成8位有符号偏移量,运行时PC加上偏移量实现跳转。

b.函数调用

汇编语言:call printf@PLT

机器语言:e8 00 00 00 00

汇编时无法确定printf的实际地址,生成32位占位符,等待链接器修正,通过重定位条目R_X86_64_PLT32指导链接器修正。

(4)寻址模式编码

a.立即数寻址:movl $0, -4(%rbp)  # c7 45 fc 00 00 00 00

b.寄存器间接寻址:mov (%rax), %rcx  # 48 8b 08

c.RIP相对寻址:lea .LC1(%rip), %rdi  # 48 8d 3d 00 00 00 00

4.4.3 小结

(1)汇编过程的核心任务是生成可重定位的机器代码,其中同一模块内的引用已解析,外部引用留待链接时处理。

(2)机器语言与汇编语言的根本区别在于地址表示方式:汇编语言使用人类可读的符号,机器语言使用数值地址或占位符。

(3)重定位机制是连接编译时和链接时的关键桥梁,确保代码可以在不同地址加载执行。

(4)理解这种映射关系对于调试、优化和系统级编程至关重要,它揭示了高级语言到底层硬件的转换过程。

4.5 本章小结

汇编阶段生成包含机器码的可重定位目标文件,但外部符号地址尚未解析,需链接阶段完成。

5章 链接

5.1 链接的概念与作用

链接器(ld)将多个.o文件及库文件合并为一个可执行文件,完成:

符号解析:将符号引用与定义关联;

重定位:将符号地址修正为最终内存地址。

注意:这儿的链接是指从 hello.o 到hello生成过程。

5.2 在Ubuntu下链接的命令

图5.1 Ubuntu下链接的命令

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

图5.2 hello的ELF格式

   

5.4 hello的虚拟地址空间

图5.3 hello的虚拟地址空间

虚拟地址范围

文件偏移

大小

ELF对应段

实际内容

权限

0x400000-0x401000

0x0

4KB

LOAD段1

ELF头、.interp、.note等

只读

0x401000-0x402000

0x1000

4KB

LOAD段2

.init、.plt、.text、.fini

可执行

0x402000-0x403000

0x2000

4KB

LOAD段3

.rodata、.eh_frame

只读

0x403000-0x404000

0x2000

4KB

LOAD段4(部分)

.data、.dynamic、.got等

读写

0x404000-0x405000

0x3000

4KB

(无直接对应)

.bss或其他运行时数据

读写

表5.1 与ELF对照分析 

5.5 链接的重定位过程分析

5.5.1 hello与hello.o的主要差异

通过对比hello.o(可重定位目标文件)和hello(可执行文件)的反汇编结果,可以观察到以下关键差异:

(1)地址解析完成

hello.o:所有外部引用和跨节引用使用占位符(全0)

hello:所有地址已被解析为具体值,包括函数调用和字符串引用

(2) 新增启动和终止代码

hello.o:仅包含用户定义的main函数

hello:添加了_start、_init、_fini、__libc_csu_init、__libc_csu_fini等代码,这些来自C运行时库

(3)PLT(过程链接表)和GOT(全局偏移表)机制

hello.o:使用@PLT标记,但实际调用地址未确定

hello:PLT条目已建立,包含完整的动态链接跳转表

(4)节合并与地址分配

hello.o:所有节起始地址为0

hello:各节被分配到具体的虚拟地址(如.text段从0x401000开始)

5.5.2 链接过程的核心机制

链接过程主要包括两个核心任务:符号解析和重定位。

(1)符号解析

链接器将每个符号引用与一个符号定义关联起来。对于hello程序:

puts、printf、getchar、atoi、exit、sleep:解析到libc共享库中的定义;

main:用户定义的函数,作为程序的入口;

.rodata中的字符串:解析到只读数据段的特定位置。

(2)重定位

链接器根据重定位条目修改目标代码,将符号地址填入需要重定位的位置。

5.5.3 具体重定位项目分析

基于之前提供的hello.o重定位条目,分析链接器如何在hello中完成重定位:

(1)第一个重定位条目:字符串引用

hello.o重定位:

  偏移量: 0x1c, 类型: R_X86_64_PC32, 符号: .rodata, 加数: -4

hello中对应的指令:

  40113e: 48 8d 3d c3 0e 00 00  lea  0xec3(%rip),%rdi 

重定位计算过程:

公式:S + A - P(对于R_X86_64_PC32类型)

S:符号地址(.rodata中字符串地址)= 0x402008

A:加数 = -4

P:重定位位置地址 = 0x40113e + 3 = 0x401141(偏移字段在指令中的地址)

计算:0x402008 - 4 - 0x401141 = 0xec3

验证:指令中的偏移值正是0xec3,正确!

作用:将lea指令的RIP相对寻址指向正确的字符串地址(错误提示信息)。

(2)第二个重定位条目:puts函数调用

hello.o重定位:

  偏移量: 0x21, 类型: R_X86_64_PLT32, 符号: puts, 加数: -4

hello中对应的指令:

  401145: e8 46 ff ff ff        callq 401090 <puts@plt>

重定位计算过程:

公式:L + A - P(对于R_X86_64_PLT32类型)

L:PLT条目地址 = 0x401090(puts@plt)

A:加数 = -4

P:重定位位置地址 = 0x401145 + 1 = 0x401146

计算:0x401090 - 4 - 0x401146 = -0xba = 0xffffff46(32位有符号数)

验证:指令中的偏移值0xffffff46(小端表示为46 ff ff ff)正是-0xba

作用:将call指令重定向到PLT条目,实现动态链接的延迟绑定。

(3)第三个重定位条目:exit函数调用

hello.o重定位:

  偏移量: 0x2b, 类型: R_X86_64_PLT32, 符号: exit, 加数: -4

hello中对应的指令:

  40114f: e8 7c ff ff ff        callq 4010d0 <exit@plt>

计算:类似puts,重定位到exit@plt地址0x4010d0。

(4)第四个重定位条目:printf格式字符串

hello.o重定位:

  偏移量: 0x5f, 类型: R_X86_64_PC32, 符号: .rodata+0x2c, 加数: +0x2c

hello中对应的指令:

  401181: 48 8d 3d b0 0e 00 00  lea  0xeb0(%rip),%rdi

计算:

S = 字符串"Hello %s %s %s\n"地址 = 0x402038

P = 0x401181 + 3 = 0x401184

偏移值 = 0x402038 - 0x401184 = 0xeb4

(5) 第五至八个重定位条目:其他函数调用

printf、atoi、sleep、getchar:都类似地重定位到相应的PLT条目

5.6 hello的执行流程

阶段

函数/地址

作用

调用者

被调用者

启动

0x4010f0 (_start)

程序入口点

操作系统

__libc_start_main

初始化

0x401000 (_init)

全局初始化

__libc_csu_init

gmon_start

C库构造

0x4011d0 (__libc_csu_init)

C++全局对象构造

__libc_start_main

_init

主程序

0x401125 (main)

用户主函数

__libc_start_main

puts/printf/atoi/sleep/getchar

错误处理

0x401090 (puts@plt)

打印错误信息

main

libc puts函数

正常输出

0x4010a0 (printf@plt)

格式化输出

main

libc printf函数

输入处理

0x4010b0 (getchar@plt)

读取字符

main

libc getchar函数

类型转换

0x4010c0 (atoi@plt)

字符串转整数

main

libc atoi函数

程序退出

0x4010d0 (exit@plt)

退出程序

main

libc exit函数

延时操作

0x4010e0 (sleep@plt)

休眠指定秒数

main

libc sleep函数

C库析构

0x401240 (__libc_csu_fini)

C++全局对象析构

exit

-

终止

0x401248 (_fini)

全局终止函数

exit

-

表5.2 调用与跳转的各个子程序名或程序地址

5.7 Hello的动态链接分析

图5.4 动态链接前后项目变化

5.8 本章小结

链接将多个模块合并为可执行文件,完成地址重定位,并支持动态链接库的延迟绑定。

6章 hello进程管理

6.1 进程的概念与作用

进程是程序执行的实例,拥有独立的地址空间、文件描述符、寄存器上下文等。OS通过进程实现多任务并发。

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

作用:

(1)读取用户输入的命令行。

(2)解析参数,判断是否为内置命令。

处理流程:

对于外部命令(如./hello),调用fork()创建子进程,再execve()加载程序。

6.3 Hello的fork进程创建过程

fork()复制当前进程(Shell)创建一个几乎完全相同的子进程,包括代码、数据、堆栈。子进程返回0,父进程返回子进程PID。

6.4 Hello的execve过程

execve("./hello", argv, envp):

加载hello的ELF文件到内存;

替换当前进程的代码段、数据段;

跳转到hello的入口地址开始执行。

6.5 Hello的进程执行

6.5.1进程创建与初始化

当用户在Shell中输入./hello 12345678 张三 13800138000 2时,Shell进程首先调用fork()系统调用创建子进程。fork()复制当前Shell的进程控制块(PCB)、地址空间和打开文件表,生成一个几乎完全相同的子进程。随后子进程调用execve()系统调用,该调用会销毁子进程原有的地址空间,根据hello程序的ELF格式重新建立内存映射:代码段映射到0x401000-0x402000(可读可执行),只读数据段映射到0x402000-0x403000,读写数据段映射到0x403000-0x404000。动态链接器/lib64/ld-linux-x86-64.so.2负责加载共享库libc,并重定位GOT表中的函数地址。完成这些初始化后,进程从入口点_start(0x4010f0)开始执行,经过C运行时初始化后调用main函数。

6.5.2 进程调度与时间片管理

Hello进程被创建后,操作系统将其加入运行队列。Linux使用完全公平调度器(CFS)管理进程执行,CFS维护一个按虚拟运行时间(vruntime)排序的红黑树。Hello作为普通进程,其权重值为1024(nice值为0的默认权重)。在典型的桌面配置中,调度周期为6毫秒,当系统中只有Shell和Hello两个权重相同的进程时,每个进程获得3毫秒的时间片。Hello进程的执行被划分为一系列时间片:前3毫秒执行初始化代码和第一次printf输出,然后时钟中断触发,内核将Hello的vruntime增加3毫秒,发现时间片用完,于是设置重新调度标志。中断返回前,调度器选择Shell进程执行。3毫秒后,Shell时间片用完,Hello再次被调度执行。这种时间片轮转在整个Hello生命周期中持续发生,大约每3毫秒切换一次,在Hello的20秒总运行时间内会发生约6666次调度切换。

6.5.3 上下文切换机制

每次调度发生时,操作系统需要执行完整的上下文切换。当Hello的时间片用完时,时钟中断触发,CPU从用户态陷入内核态。内核首先保存Hello的完整寄存器上下文:包括通用寄存器%rax、%rbx、%rcx、%rdx,栈指针%rsp,指令指针%rip,以及浮点寄存器、向量寄存器等。这些寄存器值被保存在Hello进程的内核栈中,并通过PCB的thread字段持久化。接着内核调用pick_next_task()从运行队列中选择下一个要运行的进程(通常是Shell)。如果下一个进程使用不同的地址空间,还需要切换页表基址寄存器%cr3,这会导致TLB刷新和缓存失效。然后内核从Shell的PCB中恢复其保存的寄存器值,将栈指针切换到Shell的内核栈,最后通过iretq指令返回用户态继续执行Shell。整个上下文切换过程大约消耗1000-2000个CPU周期,加上缓存失效的开销,总时间约为5-10微秒。在Hello的20秒运行期间,上下文切换的总开销约为33毫秒,占执行时间的0.17%。

6.5.4 用户态与核心态转换

Hello进程在执行过程中频繁在用户态和核心态之间转换。这种转换主要通过三种机制触发:系统调用、中断和异常。当Hello调用printf()时,实际通过PLT跳转到libc的write实现,最终执行syscall指令触发系统调用门。CPU自动切换到内核栈,保存用户态寄存器,根据系统调用号查找系统调用表,执行sys_write内核函数处理输出请求。完成服务后,内核恢复用户态寄存器返回到Hello进程。类似地,sleep(2)调用会触发sys_nanosleep系统调用,内核将Hello进程状态标记为TASK_INTERRUPTIBLE,设置2秒定时器后调用schedule()主动放弃CPU。除了显式系统调用外,时钟中断每3毫秒触发一次,强制Hello陷入内核更新时间统计,检查时间片是否用完。硬件中断如键盘输入也会导致态转换。此外,当Hello访问未映射的虚拟页面时,会发生缺页异常,陷入内核分配物理页面并建立映射。在整个执行过程中,Hello大约进行40次显式态转换(20次系统调用,每次进出内核各一次)和约6666次中断触发的态转换。

6.6 hello的异常与信号处理

异常类型

触发条件

产生信号

处理方式

硬件中断

定时器中断、键盘中断

无直接信号

内核处理,可能引起调度

系统调用异常

无效系统调用参数

返回错误码

缺页异常

访问未映射内存

SIGSEGV

内核分配物理页

算术异常

除零错误

SIGFPE

终止进程

非法指令

执行无效机器码

SIGILL

终止进程

表6.1 异常与信号处理

信号

触发方式

默认行为

说明

SIGINT (2)

Ctrl-C

终止进程

中断进程

SIGTSTP (20)

Ctrl-Z

暂停进程

挂起进程

SIGCONT (18)

fg命令

继续执行

恢复暂停的进程

SIGQUIT (3)

Ctrl-\

终止+core

强制终止

SIGKILL (9)

kill -9

强制终止

不可捕获

SIGSEGV (11)

非法内存访问

终止+core

段错误

                             表6.2 信号类型

图6.1 各命令及运行结果

6.7本章小结

进程管理使得Hello能在OS调度下执行,并支持用户交互与信号处理。

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:程序代码中的地址(如%rip值)。

线性地址(虚拟地址):经过段式管理转换后的地址。

物理地址:实际内存芯片上的地址。

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

x86-64使用平坦模式,逻辑地址直接映射为线性地址(段基址为0)。

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

通过页表实现虚拟页到物理页的映射(通常页大小为4KB)。

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

四级页表:CR3指向顶级页目录,经过4级索引找到物理页。

TLB缓存最近使用的页表项,加速转换。

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

CPU访问数据时,依次查找L1、L2、L3 Cache,未命中则访问主存。

7.6 hello进程fork时的内存映射

fork()使用写时复制(Copy-On-Write):父子进程共享物理页,直到一方尝试写入时才复制。

7.7 hello进程execve时的内存映射

execve()建立新的地址空间:

代码段、数据段从ELF文件映射;

堆、栈初始化为空。

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

访问未加载的页时触发缺页异常,OS从磁盘加载页面到内存,并更新页表。

7.9动态存储分配管理

printf内部可能调用malloc分配缓冲区,使用隐式空闲链表、分离空闲链表等算法管理堆内存。

7.10本章小结

存储管理通过虚拟内存、页表、Cache等机制,为Hello提供了透明、高效、安全的地址空间。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

    Linux将设备抽象为文件,通过VFS(虚拟文件系统)统一管理。

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

open、read、write、close:低级文件操作。

printf、scanf:标准库封装。

8.3 printf的实现分析

(1)vsprintf格式化字符串到缓冲区。

(2)write系统调用将缓冲区写入标准输出文件描述符(1)。

(3)内核通过字符设备驱动将字符写入显示器VRAM。

https://www.cnblogs.com/pianist/p/3315801.html

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

(1)键盘中断将扫描码转换为ASCII码存入缓冲区。

(2)read系统调用从标准输入(0)读取字符。

(3)getchar等待回车后返回第一个字符。

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

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

8.5本章小结

Linux的IO管理通过文件抽象和系统调用,使得Hello能方便地与键盘、显示器等设备交互

结论

1.Hello从源代码到可执行文件经历了预处理、编译、汇编、链接四个阶段。

2.在Shell中执行时,OS通过fork和execve创建进程,并为其分配虚拟地址空间。

3.进程执行过程中,CPU通过页表、TLB、Cache实现地址转换与数据访问。

4.Hello通过系统调用与标准库函数完成IO操作,最终由OS回收资源。

感悟:一个简单的Hello程序背后,是编译系统、操作系统、硬件体系结构的紧密协作。计算机系统的设计体现了分层抽象与协同工作的哲学,每一层都为上层提供简洁接口,隐藏底层复杂性。

参考文献

[1] Randal E. Bryant, David R. O’Hallaron. Computer Systems: A Programmer's Perspective. 3rd ed.

[2] GCC Manual. https://gcc.gnu.org/onlinedocs/

[3] Linux Manual Pages. https://man7.org/linux/man-pages/

[4] ELF Format Specification. http://refspecs.linuxfoundation.org/elf/elf.pdf

[5] Intel® 64 and IA-32 Architectures Software Developer Manuals.

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

原文链接:https://blog.csdn.net/kejixiaobaili/article/details/156576298

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

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