关注

程序人生-Hello’s P2P

计算机系统原理

大作业

题     目  程序人生-Hellos P2P  

专       业   AI+先进技术领军班  

学     号     2024113177        

班   级      24Q0303          

学       生      张天亮            

指 导 教 师       史先俊             

计算学部

202512

摘  要

本报告以Hello程序为研究对象,系统剖析其“从程序到进程”的全生命周期及“从源码到输出”的端到端流程。实验基于Ubuntu 22.04环境,借助gcc、gdb、readelf、objdump等工具,完成了Hello程序从预处理、编译、汇编到链接的静态处理过程,生成系列中间文件并解析其格式与作用;后续深入探究了程序加载为进程后的动态运行机制,包括进程创建与调度、存储地址空间映射、IO操作实现等核心环节。通过实验分析,揭示了高级语言程序通过软硬件协同转化为进程执行的内在逻辑,验证了计算机系统分层架构的设计思想。本次实验与分析清晰呈现了Hello程序从静态代码到动态执行的完整链路,深化了对计算机系统底层工作原理的理解,为后续深入学习系统开发与优化奠定了基础。

关键词:Hello程序;编译链接;进程管理;存储管理;IO机制;ELF格式

                          

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分

目  录

第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程序是计算机领域最基础、最经典的入门程序,核心功能是通过标准输出函数打印“Hello World”字符串,其代码简洁、逻辑清晰,却完整涵盖了高级语言程序从编写到执行的全链路核心环节,是剖析计算机系统底层工作机制的理想研究载体。

Hello程序的“P2P”过程是计算机系统程序执行的典型链路:始于Hello.c源码文件,经预处理、编译、汇编、链接四大静态处理步骤,将高级语言代码转化为可执行二进制文件;随后在Shell中执行时,操作系统通过fork创建新进程,经execve加载程序到内存,分配CPU、内存等资源并调度执行,通过IO操作输出“Hello World”结果,最终进程终止并释放资源,完成从“程序”到“进程”的完整转化。“O2O”过程则聚焦端到端逻辑:以开发者编写的C语言源码为输入,借助编译器、汇编器、链接器等工具链及操作系统的资源管理能力,最终通过显示设备输出目标结果,直观呈现了高级语言与硬件执行之间的映射关系。

1.2 环境与工具

1.2.1 硬件环境

CPU:Intel Core i9-13900HX;

内存:16GB ;

存储:1TB ;

操作系统:Windows 11 。

图1-1  CPUZ下CPU的基本信息

linux虚拟机系统下的配置:

 CPU个数:   2      物理核数:  48  逻辑处理器个数: 96  

MEM   Total:  1.9Gi  Used: 822Mi     Swap: 2.1Gi       

1.2.2 软件环境

虚拟机软件:VMware Workstation 17 Pro;

客户机操作系统:Ubuntu 22.04 LTS;

编译器套件:gcc;

调试工具:gdb(源码级调试);

ELF分析工具:objdump;

代码编辑器:Visual Studio 2022。

1.3 中间结果

1) hello.c:原始C语言源码文件,包含Hello程序的核心逻辑;

2) hello.i:预处理后的C语言文件,展开了头文件、替换了宏定义、删除了注释,为编译阶段提供标准化输入;

生成指令:gcc -E hello.c -o hello.i

3)hello.s:汇编语言文件,由编译阶段生成,将C语言代码转化为x86-64架构的汇编指令;

生成指令:gcc -S hello.i -o hello.s

4)hello.o:可重定位目标文件(ELF格式),由汇编阶段生成,包含机器码、数据及重定位信息,未解析外部符号;

生成指令:gcc -c hello.s -o hello.o

5)hello:可执行目标文件(ELF格式),由链接阶段生成,合并hello.o与系统库,解决符号重定位,可独立执行;

生成指令:gcc hello.o -o hello

1.4 本章小结

本章明确了Hello程序"P2P"与"O2O"的核心流程,界定了实验研究范围;详细梳理了实验所需的软硬件环境及工具链,确保实验的可复现性;列出了实验全流程的中间产物及其核心作用。通过本章内容,读者能够建立对实验整体框架的认知,为后续章节分阶段剖析Hello程序的生命周期奠定基础。

(第1章0.5分)


第2章 预处理

2.1 预处理的概念与作用

预处理是编译器对C语言源代码进行的前期处理过程,发生在正式编译之前,本质上是一种"文本替换与代码清理"的操作。其主要功能包括:

首先,展开头文件,将#include指令指定的头文件(如stdio.h)内容完整插入到源代码的相应位置,确保编译阶段能够正确识别库函数的声明;

其次,替换宏定义,将#define定义的宏名称替换为对应的值(例如#define N 10会将源代码中所有的N替换为10),从而简化代码编写;

再次,删除注释,移除//(单行注释)和/*...*/(多行注释),减少代码冗余,避免注释对编译过程造成干扰;

最后,处理条件编译,根据#ifdef、#ifndef等指令有选择地保留代码段。

预处理阶段不进行语法分析,仅对源代码文本进行格式化处理,最终生成.i文件,为后续编译阶段提供统一格式的输入。

2.2在Ubuntu下预处理的命令

Ubuntu环境下,使用gcc编译器的-E选项执行预处理操作,核心命令为:

gcc -E hello.c -o hello.i

命令说明:-E选项指定仅执行预处理操作,不进行后续的编译、汇编、链接;-o选项指定输出文件名为hello.i,若省略则预处理结果默认输出到终端。

执行过程如下图所示:

                                        图2-1 Ubuntu下Hello程序预处理命令执行结果

2.3 Hello的预处理结果解析

打开以Hello.c的核心代码片段为例,对比预处理前后的差异,解析预处理效果:

原始Hello.c中包含#include <stdio.h>和// hello.c注释,预处理后的hello.i文件中,stdio.h头文件的全部内容(约1000余行)被完整展开,包含printf、getchar等函数的声明,如图所示;

                                        图2-2 stdio.h头文件展开部分内容截图  

如图所示,源码中的注释被完全删除;若存在宏定义(如#define MSG "Hello"),则源码中所有MSG会被替换为"Hello"。hello.c仅二十几行,而hello.i文件行数为3089行,核心差异在于头文件的展开。此外,预处理后的代码不再包含预处理指令(#include、#define等),语法格式更统一,为后续编译阶段的语法分析提供了便利。

                                        图2-3 Hello.i文件最后面内容截图

2.4 本章小结

本章阐述了预处理的概念与核心作用,明确了预处理作为“文本格式化工具”的本质;掌握了Ubuntu环境下预处理的核心命令及操作流程;通过对比分析Hello.c与hello.i文件,验证了预处理的头文件展开、宏替换、注释删除等功能。预处理阶段消除了源码中的语法差异,生成标准化的.i文件,为后续编译阶段的顺利开展提供了基础保障。

(第2章0.5分)


第3章 编译

3.1 编译的概念与作用

编译是指从预处理后的.i文件到.s汇编语言文件的转化过程,是高级语言向机器语言过渡的关键环节。其核心作用是对预处理后的C语言代码进行语法分析、语义分析,最终将C语言的抽象语法(数据类型、运算、控制结构、函数调用等)映射为特定架构(x86-64)的汇编指令。编译过程需完成词法分析(识别关键字、标识符、常量等)、语法分析、语义分析(检查类型匹配、变量未定义等错误)、中间代码生成、代码优化及目标代码生成等步骤,最终输出汇编语言程序,实现了高级语言逻辑到底层指令的初步转化。        

3.2 在Ubuntu下编译的命令

Ubuntu环境下,使用gcc编译器的-S选项执行编译操作,核心命令为:

gcc -S hello.i -o hello.s

命令说明:-S选项指定仅执行编译操作,生成汇编语言文件后停止,不进行后续的汇编阶段;-o选项指定输出文件名为hello.s。若直接对hello.c执行编译,可使用命令gcc -S hello.c -o hello.s,此时gcc会自动先执行预处理再进行编译。

执行过程如图所示:

                                        图3-1 Ubuntu下Hello程序编译命令执行结果

3.3 Hello的编译结果解析

首先配上hello.s的内容:

         

                                                        图3-2 hello.s文件内容截图  

结合hello.s文件,按C语言核心语法元素分类解析编译器的转化逻辑,从数据、操作、控制、函数等方面分析,具体如下:

3.3.1 基础数据元素的转化

1. 常量

字符串常量:汇编中.LC0("\347\224\250\346\263\225: Hello 2024113177")和.LC1("Hello %s %s %s\n")存储在.rodata节(只读数据节,属于.data 节的只读子集)。

整型常量:检查参数个数的cmpl $5, -20(%rbp)中,$5是立即数嵌入指令,对应C中argc == 5的常量5;

2. 变量

局部变量:

main函数中argc被存储到栈:movl %edi, -20(%rbp);

argv被存储到栈:movq %rsi, -32(%rbp);

循环变量i:movl $0, -4(%rbp)。

3.3.2 赋值与类型转换的转化

1. 赋值操作

循环变量初始化movl $0, -4(%rbp):对应C中int i = 0;

函数参数传递中的赋值(如movq %rax, %rdi):本质是将寄存器值赋值给函数调用约定的参数寄存器,属于赋值操作的延伸。

2. 类型转换

汇编中call atoi@PLT:将argv[4](字符串类型)转为整型,对应C中(int)argv[4]的显式类型转换。

3.3.3 sizeof运算符的转化

编译阶段直接计算结果为立即数,如sizeof(char)=1($1)、sizeof(int)=4($4);int arr[5]的sizeof(arr)=20($20)。

本汇编代码中未直接出现sizeof运算符。

3.3.4 算术操作的转化

1. 基础运算

指针偏移计算:

addq $8, %rax(argv基地址+8字节取argv[1])、addq $16, %rax(取argv[2])、addq $24, %rax(取argv[3])。

循环变量自增addl $1, -4(%rbp)。

2. 自增自减

循环变量i的自增addl $1, -4(%rbp)。

3.3.5 逻辑与位操作的转化

1. 逻辑操作:如a&&b用testl+je实现短路求值;!a用testl+sete组合。

2. 位操作:如a&0x0F对应andl $0x0F, %eax;a>>2(有符号)对应sarl $2, %eax。3. 复合位赋值:如a&=0x0F对应andl $0x0F, -4(%rbp)。

3.3.6 关系操作的转化

相等判断:cmpl $5, -20(%rbp);对应 C 中 if (argc == 5)。

小于等于判断:cmpl $9, -4(%rbp); jle .L4:对应 C 中 i <= 9。

3.3.7 控制转移结构的转化

1. if-else

汇编中:

asm

cmpl $5, -20(%rbp)

je .L2          # argc==5则跳转到循环逻辑

leaq .LC0(%rip), %rax; call puts@PLT  # else分支:打印字符串

movl $1, %edi; call exit@PLT          # else分支:退出程序

.L2:            # if分支:循环逻辑

对应 C 中:

if ((argc == 5) {

    // 循环逻辑

} else {

    puts(("您好: Hello 2024113177");

    exit((1);

}

2. 循环(for)

汇编中 for 循环的完整转化:

汇编指令

对应C逻辑

符合的循环转化规则

movl $0, -4(%rbp)

int i = 0

循环初始化

jmp .L3

跳转到循环条件判断

循环先判断后执行

.L3: cmpl $9, -4(%rbp); jle .L4

i <= 9

cmpl+jge/jle 实现循环条件

.L4: 循环体(printf/sleep)

循环体逻辑

循环体执行

addl $1, -4(%rbp)

i++

addl 实现自增

执行完后回到.L3

jmp 条件判断

跳转回条件判断

3.3.8 数组 / 指针 操作的转化

1. 数组

movq -32(%rbp), %rax:取 argv 基地址(argv 是 char*[] 类型,存储在栈偏移 -32 处);

addq $8, %rax:argv 基地址 + 8 字节,对应 argv[1];

addq $16, %rax:对应 argv[2](2*8);

addq $24, %rax:对应 argv[3](3*8);

addq $32, %rax:对应 argv[4](4*8);

2. 指针

leaq .LC0(%rip), %rax:取字符串常量的地址;

movq (%rax), %rax:对指针解引用(如 argv[1] 是 char*,(%rax) 取指针指向的字符串)。

3.3.9 函数操作的转化

1. 参数传递

x86_64 下函数调用遵循 System V AMD64 约定,前 6 个整型/指针参数依次存 %rdi、%rsi、%rdx、%rcx、%r8、%r9。

puts 调用:leaq .LC0(%rip), %rax; movq %rax, %rdi → %rdi 传递字符串参数;

printf 调用:

leaq .LC1(%rip), %rax; movq %rax, %rdi → %rdi传递格式化字符串;

movq %rax, %rsi → %rsi传递第一个可变参数argv[1];

movq (%rax), %rdx → %rdx传递第二个可变参数argv[2];

movq (%rax), %rcx → %rcx传递第三个可变参数argv[3];

exit调用:movl $1, %edi → %rdi传递退出码 1;

atoi调用:movq %rax, %rdi → %rdi传递argv[4]字符串;

sleep调用:movl %eax, %edi → %rdi传递atoi转换后的整型值。

2. 函数调用

call puts@PLT/call printf@PLT/call exit@PLT等:均为call指令调用函数,

3. 栈帧管理

函数入口:

pushq %rbp          # 保存旧的rbp

movq %rsp, %rbp     # 新rbp指向当前rsp,建立栈帧

subq $32, %rsp      # 分配32字节栈空间(局部变量)

函数返回:

leave               # 等价于movq %rbp, %rsp + popq %rbp

ret                 # 弹出返回地址,跳转返回

4. 返回值

movl $0, %eax:main函数返回0;

其他函数(如atoi)的返回值:call atoi@PLT后返回值存%eax,再传递给sleep

3.4 本章小结

本章明确了编译阶段的核心定位——实现高级语言到汇编指令的映射;掌握了Ubuntu环境下编译的核心命令;重点分析了编译器对C语言各类数据类型及操作的转化逻辑,揭示了高级语言抽象语法与底层汇编指令的对应关系。编译阶段通过多步语法、语义分析及代码优化,生成了可被汇编器识别的汇编程序,为后续汇编阶段生成机器码奠定了基础。

(第3章2分)


第4章 汇编

4.1 汇编的概念与作用

汇编是指从.s汇编语言文件到.o可重定位目标文件的转化过程,本质是“汇编指令到机器码”的翻译过程。其核心作用是将汇编语言编写的指令序列,逐行翻译为对应架构的二进制机器码(如x86-64架构的机器码为1-15字节不等),同时生成ELF格式的可重定位目标文件。该文件包含代码段(.text,存储机器码)、数据段(.data,存储已初始化数据)、重定位信息段(.rel.text、.rel.data,存储未解析的外部符号信息)、符号表(.symtab,存储变量、函数名及对应地址)等。汇编阶段不解决外部符号引用(如printf函数),仅完成汇编指令到机器码的直接映射,生成的.o文件需经链接阶段处理后才能执行。

4.2 在Ubuntu下汇编的命令

Ubuntu环境下,使用gcc编译器的-c选项执行汇编操作,核心命令为:

gcc -c hello.s -o hello.o

命令说明:-c选项指定仅执行汇编操作,生成可重定位目标文件后停止,不进行后续的链接阶段;-o选项指定输出文件名为hello.o。若直接对hello.c执行汇编,可使用命令gcc -c hello.c -o hello.o,此时gcc会自动依次执行预处理、编译、汇编三个阶段。

执行过程:

                                           图4-1 Ubuntu下Hello程序汇编命令执行结果

4.3 可重定位目标elf格式

使用readelf -a hello.o命令分析hello.o的ELF格式,结果如下:

1.ELF头:

ELF头(ELF header)作为ELF格式文件的核心元数据结构,以16字节的固定长度标识序列起始,该序列精准定义了文件所属系统的字长(32/64位)与字节序(小端/大端模式)。ELF头后续字段则承载了链接器解析目标文件的关键信息,包括ELF头自身的字节大小、目标文件的功能类型(可重定位文件、可执行文件或共享库)、目标架构的机器类型(如x86-64)、节头部表在文件中的偏移地址,以及节头部表的条目尺寸与总数量。值得注意的是,目标文件中各节的物理位置与内存大小均由节头部表统一描述,表内每个条目对应一个节,且所有条目保持固定的字节长度。

ELF头展示如下:

                                                        图4-2:ELF头截图

2.节区表:

节区表(Section Header Table)记录了文件中所有节的属性信息,包括各节的名称、类型、大小、在文件中的偏移量、读写权限标志及对齐方式。每个节区表条目固定占用40字节,包含节名称字符串表索引、节地址对齐要求及链接时所需的重定位信息标记。

通过readelf -a hello.o命令可查看.text、.data、.bss、.rodata等节的具体信息,其中.text节包含编译生成的机器指令,.data节存储已初始化的全局和静态变量,.bss节预留未初始化变量的内存空间,.rodata节存放只读数据如字符串常量。节区表还指明各节在内存中的对齐策略,确保访问效率与系统兼容性。通过对节区表的解析,链接器能够准确定位代码与数据位置,完成符号解析与重定位计算,为后续的链接过程奠定基础。

                                                        图4-3:节区表截图  

3.重定位信息:

重定位信息是ELF文件实现符号动态解析的核心机制,其本质是记录程序中未决外部符号(如跨模块函数、全局变量)的位置标记与修正规则。在编译阶段,编译器仅能确定符号的相对引用关系,无法预知其最终内存地址,因此会在目标文件中插入重定位条目,详细标注符号所在的代码/数据偏移、符号类型及修正方式。当程序进行链接(静态/动态)或加载时,链接器/加载器会依据这些重定位信息,将符号引用替换为实际内存地址,最终完成程序的地址空间绑定,确保跨模块资源访问的正确性。

                                                        图4-4:重定位表截图  

4.符号表:

符号表是程序调试与链接过程中的核心元数据结构,它系统记录了程序内所有函数、变量等符号的名称、内存地址、数据类型及作用域等关键信息。作为调试器、链接器等开发工具的"程序地图",符号表为工具解析程序内部结构提供了精准依据,使其能够高效定位和操作各类符号。例如,调试器可通过符号表快速匹配函数名称与对应内存地址,从而在调试时便捷地设置断点、追踪函数调用流程或查看变量实时值;链接器则依赖符号表完成跨模块符号的解析与地址绑定,确保程序各部分能正确协同工作。

                                                        图4-5:符号表截图  

4.4 Hello.o的结果解析通过对hello.o文件的深入解析,

使用objdump -d -r hello.o命令对hello.o进行反汇编,对比第3章的hello.s文件,分析结果如下:

                                                         图4-6:反汇编命令执行

1. 机器语言与汇编语言的基本映射

机器语言是 CPU 可直接执行的二进制指令序列(十六进制编码形式),汇编语言是其符号化表示,二者为一一对应的硬映射关系,核心映射示例及说明如下:

映射核心逻辑:汇编助记符(如pushq/movq)与机器码操作码一一对应,寄存器、立即数、本地内存偏移等操作数在机器码中以二进制编码形式存在,汇编仅做符号化替换,未改变底层逻辑。

2. 分支转移的差异

分支转移的核心差异是 “汇编用符号标签标记目标,机器码用相对偏移量编码目标”,具体代码示例及差异说明如下:

汇编语言代码:

机器语言代码:

差异说明:

汇编层面:通过.L2/.L3/.L4等符号标签直观标记分支目标,无需计算地址偏移,提升可读性;

机器码层面:无符号概念,分支操作数是 “相对于当前指令指针 rip 的字节偏移量”,偏移量由汇编器自动计算并编码为二进制数值。

3. 函数调用的差异

函数调用的核心差异是 “汇编用符号表示调用目标,机器码用占位符 + 重定位标记暂存目标”,具体代码示例及差异说明如下:

汇编语言代码:

机器语言代码:

差异说明:

汇编层面:用puts@PLT/printf@PLT等符号直接指向外部函数,无需关注地址细节;

机器码层面:因外部函数最终内存地址未确定,调用操作数用00 00 00 00作为占位符,同时通过重定位标记(如R_X86_64_PLT32 puts-0x4)记录链接时的修正规则,链接阶段会将占位符替换为函数的实际 rip 相对偏移地址。

4. 操作数处理的差异

操作数处理差异分为 “本地确定操作数” 和 “外部未确定操作数” 两类,核心差异是外部操作数在汇编中用符号表示,在机器码中用占位符 + 重定位标记表示,具体代码示例如下:

类别 1:本地确定操作数

汇编语言代码:

机器语言代码:

类别 2:外部未确定操作

汇编语言代码:

机器语言代码:

差异说明

本地确定操作数:汇编与机器码仅表现形式不同,操作数逻辑完全一致;

外部未确定操作数:汇编用.LC0/puts@PLT等符号简化地址管理,机器码因地址未确定,用00 00 00 00作为占位符,并通过重定位标记标注链接时的地址修正规则。

4.5 本章小结

本章阐明了汇编阶段的核心功能——将汇编指令翻译为机器码并生成ELF格式可重定位目标文件;掌握了Ubuntu环境下汇编的核心命令;深入解析了ELF可重定位目标文件的结构,明确了各关键节的作用;通过反汇编对比,验证了机器码与汇编指令的一一映射关系,分析了重定位项的存在意义。汇编阶段完成了从符号化汇编指令到二进制机器码的转化,生成的hello.o文件虽包含完整的代码和数据,但因未解决外部符号引用,无法直接执行,为后续链接阶段的符号解析和地址重定位提供了基础。

(第4章1分)


5链接

5.1 链接的概念与作用

链接是将一个或多个可重定位目标文件(如hello.o)与系统库(如C标准库libc.so)合并,生成可执行目标文件的过程。其核心作用包括:一是符号解析,查找并绑定目标文件中未定义的符号,将其与库文件中的对应符号关联;二是重定位,根据符号的最终地址,调整目标文件中引用该符号的指令地址,填充占位符,使代码能正确访问外部函数和变量。链接分为静态链接和动态链接:静态链接将库文件的代码完整复制到可执行文件中,生成的文件体积较大但不依赖外部库;动态链接仅在可执行文件中记录库文件的引用信息,运行时由动态链接器加载库文件并完成重定位,实现库的共享复用,减少文件体积。Hello程序采用动态链接方式,依赖libc.so库中的printf函数。

5.2 在Ubuntu下链接的命令

Ubuntu环境下,可使用ld命令直接链接,也可通过gcc间接链接(gcc内部调用ld)。核心命令如下:

ld hello.o -lc -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o hello

命令说明:-lc指定链接C标准库;-dynamic-linker指定动态链接器路径(x86-64架构默认路径为/lib64/ld-linux-x86-64.so.2);-o指定输出可执行文件名为hello。

链接过程截图:

                                图5-1 Ubuntu下Hello程序链接及运行结果

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

使用readelf -a hello命令分析可执行目标文件的ELF格式,结果如下:

1. ELF头部:文件类型为EXEC(可执行文件),架构为x86-64,入口地址为0x4010d0,指定了程序头表和节表的偏移量及大小。ELF头起始于一个16字节的序列,此序列用于描述生成该文件的系统的字大小与字节顺序。ELF头的其余部分包含辅助链接器进行语法分析和目标文件解释的信息,如ELF头的尺寸、目标文件的类别、机器类型、节头部表的文件偏移量,以及节头部表里条目的尺寸与数目。各节的位置与大小由节头部表来描述,目标文件中的每个节在节头部表中都有一个固定尺寸的条目。

                                                图5-2可执行文件hello的ELF头.

2.程序头表:包含多个LOAD段(可加载到内存的段),分为代码段(可读可执行)和数据段(可读可写),每个LOAD段指定了虚拟地址、物理地址、文件大小、内存大小等信息,内核加载程序时会根据程序头表将对应段映射到进程虚拟地址空间。

       

                                                图5-3可执行文件hello的程序头表.

3.节头表:text节包含程序的机器指令,位于代码段中;.data节存储已初始化的全局变量和静态变量;.bss节用于未初始化的静态变量,在文件中不占空间,加载时由系统置零;.rodata节存放只读数据,如字符串常量;各节区内容布局严格遵循x86-64 ABI规范,确保运行时正确映射与访问。got与.plt节分别用于全局偏移表和过程链接表,支撑动态链接函数的延迟绑定机制;.symtab节包含符号表信息,.strtab节存储对应的字符串表,二者协同完成符号解析。

                                                图5-4可执行文件hello的节头表.

4.重定位节:rela.text节包含.text节的重定位信息,记录了代码中需要动态链接器在加载时修正的地址引用,如调用外部函数printf的相对偏移;每个重定位条目指明了修改位置、符号索引及重定位类型,确保运行时正确解析共享库函数地址。.rela.dyn则处理数据段中的全局偏移表GOT相关重定位,支持动态链接过程中的符号绑定与地址填充,保障程序在加载时能准确访问共享库中的变量与函数。重定位机制在程序加载时由动态链接器解析,完成符号地址的最终绑定,确保程序调用外部函数或引用共享库变量时跳转到正确位置。

                                                图5-5可执行文件hello的重定位节表.

5.符号表:symtab节中的符号表条目包含符号名称、值(符号对应地址或偏移)、大小、类型、绑定属性及所在节区索引,用于链接时解析全局符号引用;其中函数与全局变量符号标记为GLOBAL绑定,局部符号为LOCAL绑定,支持链接器正确合并多个目标文件。符号表与重定位表协同工作,确保外部符号在动态链接时被正确解析与修补,保障程序运行时的函数调用与数据访问一致性。

.                                          图5-6可执行文件hello的符号表结构.

5.4 hello的虚拟地址空间

使用gdb hello加载程序,执行info files命令查看虚拟地址空间分布,结果如下:.text段起始地址为0x00000000004010d0,.rodata段起始地址为0x00000000000402000,以此类推。虚拟地址空间的分配遵循x86-64架构规范:低地址区域为代码段、数据段,高地址区域为栈、共享库,中间为堆区域。程序头表中LOAD段的虚拟地址与info files命令显示的地址一致,验证了ELF文件格式与虚拟地址空间的映射关系。

                                        图5-7 hello程序加载后的虚拟地址空间布局.

5.5 链接的重定位过程分析

hello.o 是可重定位目标文件,仅包含业务代码指令和未解析的重定位项,指令地址以 0 为基准;hello 是可执行文件,经链接后完成地址分配、重定位修正、动态链接表构建,指令地址为绝对虚拟地址,无重定位项,可直接被系统加载执行。二者核心差异及链接过程如下:

1.核心差异对比

重定位项:hello.o 的 call 指令操作数为占位符 0x0,并附带 R_X86_64_PLT32 重定位标记,lea 指令(取字符串地址)也有 R_X86_64_PC32 标记;hello 中这些占位符被替换为实际偏移量,无任何重定位标记。

                                        图5-8:hello.o部分反汇编代码

节区构成:hello.o 仅包含 .text/.rodata 等基础节区;hello 新增 .plt/.plt.sec 节区(动态链接过程链接表),用于外部函数的动态地址解析。

                                        图5-9:hello相比hello.o新增的节区

2.链接过程与重定位修正逻辑

链接器核心工作:解析hello.o重定位表,修正占位符为实际地址,完成地址分配与节区合并,过程如下:

地址空间分配与节区合并:合并hello.o与CRT文件节区,分配连续虚拟地址,重算.text节分支偏移量(节内分支无需额外重定位)。

字符串地址重定位:根据R_X86_64_PC32标记,确定.rodata节字符串绝对地址,计算指令到目标地址偏移量,修正lea指令完成绑定。

外部函数调用重定位:根据R_X86_64_PLT32标记,构建PLT入口,计算call指令到PLT入口偏移量,修正指令并构建.plt表头实现延迟绑定。

动态链接表初始化:生成.plt和.plt.sec,PLT入口指向GOT;运行时动态链接器通过GOT解析libc.so函数地址完成调用。

链接本质:地址绑定与重定位修正,将hello.o转换为可执行文件,通过PLT/GOT实现外部库动态链接。

5.6 hello的执行流程

使用gdb调试跟踪hello的执行流程,步骤及结果如下:

1.执行break _start设置断点,运行程序后停在入口点_start;

2.执行step命令跟踪,_start调用__libc_start_main函数,该函数负责初始化进程环境(设置栈、初始化全局变量等);

3.__libc_start_main调用main函数,程序进入用户代码逻辑,执行printf("Hello World\n");

4.main函数执行完毕返回,__libc_start_main调用exit函数终止进程。关键函数调用链:_start → __libc_start_main → main → printf → exit。

5.7 Hello的动态链接分析

  1. 程序启动时,内核加载可执行文件和动态链接器ld-linux-x86-64.so.2;
  2.  动态链接器解析可执行文件中的动态符号,查找libc.so.6库中的printf函数地址;
  3. 动态链接器完成重定位,将printf函数的真实地址填充到call指令中;
  4. 动态链接完成后,程序开始执行用户代码。对比动态链接前后的call printf指令地址:链接前为占位符,链接后为printf函数的真实虚拟地址。

5.8 本章小结

本章明确了链接的核心功能——符号解析与重定位,区分了静态链接与动态链接的差异;掌握了Ubuntu环境下链接的核心命令;深入解析了ELF可执行文件的格式及虚拟地址空间分布;通过反汇编对比和调试跟踪,详细分析了重定位过程、程序执行流程及动态链接机制。链接阶段将不可执行的目标文件转化为可独立运行的可执行文件,动态链接实现了库的共享复用,提升了系统资源利用率,完成了从“程序”到“可执行文件”的关键转化。

(第5章1分)


6hello进程管理

6.1 进程的概念与作用

进程是程序的执行实例,是操作系统进行资源分配和调度的基本单位。其核心作用是为程序执行提供独立的运行环境,包括独立的虚拟地址空间、CPU时间片、文件描述符等资源,实现不同程序的执行隔离。程序是静态的代码和数据集合,而进程是动态的执行过程,具有生命周期。当用户执行./hello时,操作系统为Hello程序创建一个新进程,分配所需资源,调度其执行,进程终止后释放所有资源,确保其他程序的运行不受影响。

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

Shell是用户与操作系统的交互接口,本质是命令解释器。其核心作用是解析用户输入的命令,调用对应的程序并管理其执行。处理./hello命令的流程如下:1. 读取输入:bash从标准输入读取用户输入的./hello命令;

  1. 解析命令:bash解析命令,识别出可执行文件为当前目录下的hello;
  2. 创建子进程:bash通过fork系统调用创建一个子进程;
  3. 执行程序:子进程通过execve系统调用加载并执行hello程序,替换子进程的代码和数据;
  4. 等待终止:父进程(bash)通过waitpid系统调用等待子进程终止;
  5. 输出结果:子进程终止后,bash输出命令执行结果(若有),并等待下一条命令输入。

6.3 Hello的fork进程创建过程

当bash执行fork系统调用创建子进程时,内核完成以下操作:

  1. 复制PCB(进程控制块):内核为子进程创建新的PCB,复制父进程PCB中的大部分信息(如进程状态、优先级、文件描述符表等);
  2. 分配进程ID:为子进程分配唯一的PID(进程标识符),与父进程的PID区分;
  3. 复制地址空间:采用写时复制机制,父子进程共享物理内存页,仅当某进程修改数据时,才复制该页到新的物理内存,避免创建时的大量内存复制,提升效率;
  4. 设置进程关系:将子进程的父进程ID设置为父进程的PID,建立父子进程关系;
  5. 唤醒子进程:将子进程状态设置为就绪态,放入就绪队列,等待CPU调度。fork调用返回后,父子进程同时执行,父进程继续执行bash逻辑,子进程准备执行execve加载hello。

6.4 Hello的execve过程

子进程执行execve系统调用时,内核完成以下操作:

  1. 解析可执行文件:内核读取hello的ELF头部,验证文件合法性,获取程序头表信息;
  2. 销毁旧地址空间:释放子进程原有的虚拟地址空间(继承自父进程的bash地址空间);
  3. 创建新地址空间:为子进程创建新的虚拟地址空间,根据ELF程序头表,将hello的.text段、.data段等映射到对应虚拟地址;
  4. 映射共享库:加载动态链接器,由动态链接器加载hello依赖的libc.so.6库,映射到虚拟地址空间;
  5. 设置程序计数器:将CPU的程序计数器(PC)设置为hello的入口地址(_start函数地址),后续CPU调度时将从该地址开始执行。execve执行完成后,子进程的地址空间完全被hello程序替换,开始执行hello的代码。

6.5 Hello的进程执行

Hello进程的执行依赖操作系统的CPU调度机制:

  1. 进程状态转换:子进程创建后处于就绪态,当CPU空闲时,调度器根据进程优先级选择该进程,将其状态转为运行态,分配CPU时间片;
  2. 执行过程:CPU按照程序计数器指向的地址依次执行指令,从_start开始,经__libc_start_main初始化后进入main函数,执行printf输出结果,main函数返回后执行exit终止;
  3. 调度切换:若时间片耗尽,调度器将进程状态转为就绪态,保存进程上下文(寄存器值、程序计数器等),选择其他就绪进程执行;当再次调度到该进程时,恢复上下文,从断点继续执行;
  4. 态转换:执行printf时需调用系统调用,进程从用户态陷入核心态,内核完成IO操作后返回用户态继续执行。

6.6 hello的异常与信号处理

 hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

 程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

 6.6.1 hello执行过程中的异常类型与对应信号

hello程序运行过程中,异常主要分为用户主动触发的信号异常和程序逻辑/系统触发的异常两类,核心异常、对应信号如下表:

                                                图6-1:异常类型与对应信号

6.6.2运行时按下CTRL-C

按下Ctrl-C时,终端驱动程序会向当前前台进程组发送SIGINT信号,默认行为是终止进程。hello进程接收到该信号后将结束执行,并由操作系统回收其资源。

                                                图6.-2:运行时按下Ctrl-C

6.6.3运行时按下Ctrl-Z

按下Ctrl-Z时,终端驱动程序向当前前台进程组发送SIGTSTP信号,默认行为是暂停进程。hello进程被挂起,进入停止态,其子进程也随之暂停。

                                                图6-3:运行时按下Ctrl-Z  

此时可执行ps查看进程状态,显示为“T”;

                                                图6-4:ps命令输出结果  

jobs命令可见其在作业列表中标记为已停止;

                                                图6-5:jobs命令输出结果  

pstree可观察到该进程仍存在于进程树中;

                                                图6-6:pstree命令输出结果  

通过fg命令可将其转至前台继续运行;

                                                图6-7:fg命令恢复进程输出结果

使用kill发送SIGKILL信号彻底终止。系统保存其上下文,等待后续调度或显式恢复操作。

                                                图6-8:kill命令终止进程输出结果。

6.6.4不停乱按

不停乱按时,终端驱动会将输入缓存至行缓冲区,直至回车键按下后一次性送入程序。若在标准输入读取前终止或挂起进程,则输入数据被丢弃。乱按产生的字符不会触发异常。

                                                 图6-9:不停乱按结果

6.7本章小结

本章阐述了进程的概念与核心作用,解析了Shell-bash的命令处理流程;详细分析了Hello进程的创建(fork)、程序加载(execve)、执行调度及异常信号处理的完整过程;验证了写时复制、进程状态转换、用户态与核心态切换等关键机制。进程管理是操作系统实现程序并发执行的核心,通过fork与execve的协同,实现了程序与进程的分离,确保了执行环境的独立性和资源的有效利用。

(第6章2分)


7hello的存储管理

7.1 hello的存储器地址空间

Hello进程的存储器地址空间包含四类核心地址:

  1. 逻辑地址:程序代码中使用的地址(如变量&a、函数地址),是相对于程序自身的偏移地址,未经过地址转换;
  2. 线性地址:逻辑地址经段式管理转换后的地址,x86-64架构下逻辑地址与线性地址一致(段基址为0);
  3. 虚拟地址:进程视角的地址,即线性地址,进程认为自己独占虚拟地址空间,与其他进程隔离;
  4. 物理地址:内存硬件的真实地址,线性地址经页式管理转换后得到,是CPU访问内存的实际地址。四类地址的转化流程:逻辑地址 → 线性地址(段式转换) → 物理地址(页式转换),最终实现程序地址到物理内存地址的映射。

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

x86-64架构下的段式管理机制简化了传统32位架构的复杂设计,核心流程如下:

1. 段选择器:段寄存器(如CS、DS)中存储段选择器,用于索引GDT(全局描述符表)或LDT(局部描述符表);

2. 段描述符:GDT中存储段描述符,包含段基址、段限长、权限等信息;x86-64架构下,用户态进程的代码段(CS)和数据段(DS)描述符的段基址均为0,段限长为2^64-1;

3. 线性地址计算:由于段基址为0,线性地址 = 段基址 + 逻辑地址(偏移量) = 逻辑地址,因此逻辑地址与线性地址完全一致。

段式管理的核心作用从地址转换转变为权限控制,确保代码段不可写、数据段不可执行,保障进程安全。

                                                图7-1 段式管理示意图  

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

x86-64架构采用四级页式管理(页全局目录、页上级目录、页中间目录、页表),将64位线性地址划分为多个部分,用于索引各级页表。核心转换流程:

  1. 线性地址拆分:64位线性地址拆分为页全局目录号(9位)、页上级目录号(9位)、页中间目录号(9位)、页表号(9位)、页内偏移(12位);
  2. 页表查找:CPU从CR3寄存器获取页全局目录的物理地址,根据页全局目录号索引到对应页全局目录项,得到页上级目录的物理地址;依次类推,通过页上级目录号、页中间目录号、页表号索引,最终得到页表项;
  3. 物理地址计算:页表项中存储物理页框号(40位),物理地址 = 物理页框号 << 12 + 页内偏移;
  4. 权限检查:各级页表项包含权限位(如可读、可写、可执行),CPU检查进程是否有权限访问该地址,无权限则触发异常。

                                                图7-2 页式管理地址转换流程示意图  

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

TLB(快表)是CPU中的高速缓存,用于缓存近期访问的页表项,减少四级页表的查找次数,提升地址转换效率。VA(虚拟地址)到PA(物理地址)的完整转换流程:

  1. TLB查找:CPU收到虚拟地址后,先查询TLB,若TLB中存在该虚拟地址对应的页表项(命中),则直接提取物理页框号,计算物理地址,无需访问内存中的四级页表;
  2. TLB未命中:若TLB中无对应页表项,则执行四级页表查找流程,从内存中逐级索引页表,获取物理页框号;
  3. TLB更新:将查找得到的页表项缓存到TLB中,覆盖近期最少使用的项,以便后续访问时快速命中;
  4. 物理地址生成:结合物理页框号和页内偏移,生成最终的物理地址。Hello程序执行时,频繁访问的.text段和.data段地址会被缓存到TLB,显著提升指令和数据的访问速度。

                                                图7-3 ARMv8的四级页表的转换

  

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

CPU与内存之间存在三级Cache(L1、L2、L3),用于缓解CPU与内存的速度差异,提升访问效率。Hello程序的物理内存访问流程:

  1. CPU生成物理地址后,先访问L1 Cache(最快,容量最小);
  2. 若L1 Cache命中,直接从L1读取数据,完成访问;
  3. 若L1未命中,访问L2 Cache,命中则读取数据,并将数据缓存到L1; 4.若L2未命中,访问L3 Cache(最慢,容量最大),命中则读取数据,缓存到L2和L1;

5.若L3未命中,访问内存,读取数据后缓存到L3、L2、L1,再返回CPU。

Cache采用局部性原理(时间局部性、空间局部性),Hello程序中连续执行的指令和连续访问的数据能高效命中Cache,大幅提升程序执行效率。

                                                图7-4存储器金字塔结构

7.6 hello进程fork时的内存映射

Hello进程通过fork创建子进程时,采用写时复制(Copy-on-Write)的内存映射机制,核心流程:

  1. 共享物理内存:fork执行时,内核不为子进程分配新的物理内存,而是让父子进程共享所有物理内存页(代码段、数据段、堆、栈等);
  2. 标记写保护:将共享的物理内存页标记为写保护(只读);
  3. 写操作触发复制:当父进程或子进程修改某共享页的数据时,CPU触发页故障,内核捕获故障后,为修改方分配新的物理内存页,复制原页数据到新页,解除新页的写保护,将页表项指向新页;
  4. 独立内存空间:经过多次写操作后,父子进程的修改数据页完全独立,未修改的页仍保持共享。

写时复制机制避免了fork时大量的内存复制,减少了资源消耗,提升了进程创建效率。

 

                                                图7-5写时复制机制示意图  

7.7 hello进程execve时的内存映射

execve加载hello程序时,内核为子进程创建新的虚拟地址空间并完成内存映射,核心流程:

  1. 销毁旧映射:释放子进程继承自父进程(bash)的虚拟地址空间映射(代码段、数据段等);
  2. 映射可执行文件:根据hello的ELF程序头表,将.text段(代码)映射到0x401000开始的虚拟地址(可读可执行),.data段(已初始化数据)映射到0x404000开始的虚拟地址(可读可写),.bss段(未初始化数据)映射到.data段之后(可读可写);
  3. 映射堆和栈:映射堆区域,映射栈区域(从高地址开始,初始大小固定,可动态扩展);
  4. 映射共享库:加载动态链接器,由动态链接器加载libc.so.6等共享库,映射到虚拟地址空间的高地址区域(与栈相邻);
  5. 建立页表:为各映射区域建立对应的页表项,标记权限,完成虚拟地址到物理地址的映射准备(实际物理内存加载采用缺页机制)

                                                图7-6加载执行文件时栈的变化

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

Hello进程执行时,若访问的虚拟地址对应的物理页未加载到内存(如首次访问.text段某页、动态分配堆内存),则触发缺页故障(页错误),内核通过缺页中断处理流程解决:

  1. 触发中断:CPU执行指令时发现虚拟地址对应的页表项无效,触发缺页中断,陷入核心态;
  2. 保存上下文:内核保存当前进程的上下文;
  3. 检查地址合法性:内核检查虚拟地址是否在进程的地址空间内,若非法则发送SIGSEGV信号终止进程(段错误);
  4. 分配物理内存:若地址合法,内核为进程分配空闲的物理内存页;
  5. 加载数据:将磁盘中对应的数据(如hello的.text段数据、swap分区数据)加载到新分配的物理内存页;
  6. 更新页表:修改页表项,将虚拟地址映射到新的物理页框号,标记页表项为有效;
  7. 恢复上下文:内核恢复进程的上下文,返回用户态,让进程从触发缺页故障的指令重新执行。缺页机制实现了“按需加载”,减少了程序启动时的内存占用,提升了系统内存利用率。

                                                图7-7 缺页中断处理流程示意图  

7.9动态存储分配管理

Hello程序中printf函数的实现依赖动态内存分配,底层通过malloc函数申请动态内存,malloc的实现基于内核的brk或mmap系统调用,采用空闲块链表管理内存。核心机制:

  1. 内存分配:当申请的内存较小时,malloc通过调整堆顶指针扩展堆空间,从空闲块链表中查找合适的空闲块,分配给用户;
  2. 内存释放:free函数将用户释放的内存块归还给空闲块链表,若相邻存在空闲块则合并,减少内存碎片;
  3. 大内存处理:当申请的内存较大时,malloc通过mmap系统调用直接映射匿名内存区域(不占用堆空间),释放时通过munmap系统调用直接解除映射;
  4. 内存池优化:为提升效率,malloc维护内存池,预分配一定大小的内存块,避免频繁调用brk/mmap系统调用。动态存储分配机制实现了内存的灵活使用,满足程序运行时的动态内存需求。

                                                图7-8 动态内存分配概述图

7.10本章小结

本章系统阐述了Hello进程的四类地址概念及转化关系;详细分析了x86-64架构下段式管理、四级页式管理的地址转换流程;探究了TLB和三级Cache对内存访问效率的提升机制;验证了fork时的写时复制、execve时的内存映射及缺页中断处理流程;简述了动态存储分配的核心机制。存储管理的核心目标是通过多层次的地址转换和缓存机制,实现虚拟地址到物理地址的高效、安全映射,优化内存利用率和访问速度,为进程执行提供稳定的内存环境。

(第7章 2分)


8hello的IO管理

8.1 Linux的IO设备管理方法

Linux采用“一切皆文件”的IO设备管理思想,将所有IO设备(键盘、显示器、磁盘等)抽象为文件,通过统一的文件描述符(fd)机制管理。核心方法:

  1. 设备抽象:每种设备对应一个设备文件(位于/dev目录下),如键盘对应/dev/console,显示器对应/dev/tty,用户程序通过操作设备文件间接访问设备;2.设备驱动:内核通过设备驱动程序实现硬件与内核的交互,驱动程序隐藏设备硬件细节,为内核提供统一的接口;

3.文件描述符:进程通过文件描述符标识打开的文件/设备,标准输入(stdin)fd=0,标准输出(stdout)fd=1,标准错误(stderr)fd=2;

4.分层架构:IO管理分为用户层、内核层、驱动层、硬件层,用户程序通过系统调用陷入内核,内核调用驱动程序,驱动程序控制硬件执行IO操作,实现了用户程序与硬件的隔离。

                                                图8-1 Linux 设备示意图  

8.2 简述Unix IO接口及其函数

Unix IO接口是Linux IO管理的核心,提供了一组简洁、统一的函数,适用于所有设备文件,核心函数包括:

  1. open:打开文件或设备,创建文件描述符,返回fd;函数原型:int open(const char *pathname, int flags, mode_t mode);flags指定打开模式(如O_RDONLY只读、O_WRONLY只写、O_RDWR读写);
  2. read:从fd对应的文件/设备读取数据到缓冲区;函数原型:ssize_t read(int fd, void *buf, size_t count);返回实际读取的字节数;
  3. write:将缓冲区的数据写入fd对应的文件/设备;函数原型:ssize_t write(int fd, const void *buf, size_t count);返回实际写入的字节数;
  4. close:关闭fd对应的文件/设备,释放相关资源;函数原型:int close(int fd);
  5. ioctl:设备控制函数,用于设置或获取设备特定参数;函数原型:int ioctl(int fd, unsigned long request, ...)。

Unix IO接口的特点是无缓冲(或少量缓冲)、支持阻塞/非阻塞模式,为所有IO设备提供了统一的操作接口。

8.3 printf的实现分析

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

printf 函数的实现本质是 “用户层格式化处理 - 内核层系统调用 - 硬件层显示输出” 的三级联动流程,核心围绕可变参数解析、格式转换、特权级切换与显存操作展开。

printf 的核心目标是接收可变个数、可变类型的参数,按指定格式字符串(fmt)转换为统一格式的字符串,并通过硬件显示器输出。其实现依赖 “分层解耦” 设计:用户层负责参数解析与格式化,内核层负责权限管控与系统调用转发,硬件层负责最终的像素渲染与显示,确保用户态程序无法直接操作硬件,保障系统安全。

1. 关键流程与核心组件

可变参数解析与格式化(vsprintf 核心作用)

printf 函数原型为 int printf(const char *fmt, ...),其中 ... 表示可变参数,其解析依赖 C 语言函数参数的栈布局规则(从右往左压栈、栈地址从高到低增长)。核心逻辑如下:

可变参数访问:通过 va_list(本质为字符指针)遍历栈中可变参数,通过 (char*)(&fmt) + 4(32 位系统)跳过固定参数 fmt 的栈地址,直接定位第一个可变参数的内存位置,后续通过指针偏移(如 p_next_arg += 4)依次访问后续参数;

格式转换:vsprintf 遍历格式字符串 fmt,非 % 字符直接写入输出缓冲区,遇到格式符(如 %x/%s/%d)时,从可变参数中取出对应值,转换为目标格式字符串(如整数转 16 进制字符串)后写入缓冲区;

结果输出:vsprintf 返回格式化后字符串的长度,为后续写入操作提供字节数依据。

2.系统调用与特权级切换(write 函数的核心作用)

用户态程序无硬件操作权限,需通过 write 函数触发系统调用,完成用户态到内核态的切换,核心流程如下:

系统调用封装:write 函数通过汇编指令设置寄存器(eax 存储系统调用号,ebx/ecx 传递缓冲区地址与字节数),随后触发中断(32 位 Linux 为 int 0x80,64 位为 syscall);

中断处理与特权级切换:中断触发后,CPU 从用户态(特权级 3)切换到内核态(特权级 0),通过中断门跳转到内核的系统调用处理函数 sys_call;

内核层转发:sys_call 保存用户态进程上下文,通过系统调用表(sys_call_table)定位到具体的写操作实现(sys_write),将用户缓冲区数据复制到内核缓冲区,避免用户态非法访问内核资源。

3.硬件层显示输出(驱动与显存操作)

内核通过显示驱动程序完成最终的硬件输出,核心逻辑如下:

字符与像素转换:驱动程序解析内核缓冲区中的 ASCII 字符,从字模库中获取对应字符的点阵数据,并转换为 RGB 颜色信息;

显存(VRAM)写入:显存是专门存储显示数据的内存区域,驱动将 RGB 像素数据按显存地址映射规则写入 VRAM(如 VGA 文本模式下 VRAM 起始地址为 0xB8000);

硬件渲染与显示:显示芯片(GPU)按固定刷新频率(如 60Hz)逐行读取 VRAM 中的像素数据,通过信号线(HDMI/VGA)传输给显示器,液晶面板根据 RGB 信号控制每个像素的亮度与颜色,最终呈现格式化字符串。

                                                  图8-2 printf函数的代码示例

8.4 getchar的实现分析

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

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

若Hello程序中包含getchar()(用于读取用户输入),其底层实现同样依赖Unix IO接口,核心流程:

  1. 等待输入:getchar是标准IO函数,采用行缓冲,若无缓冲区数据则调用read系统调用,传入fd=0(标准输入),等待用户输入;
  2. 键盘中断:用户按下键盘按键时,键盘产生中断信号,CPU陷入核心态,执行键盘中断处理子程序;
  3. 数据转换:中断处理程序读取键盘扫描码,转换为对应的ASCII码,将ASCII码存入系统键盘缓冲区;
  4. 读取数据:read系统调用从系统键盘缓冲区读取ASCII码,存入用户程序的缓冲区;
  5. 返回结果:getchar从用户级缓冲区读取一个字符,返回给程序;当用户按下回车键时,read系统调用返回,表明一行输入完成。若程序执行时未输入字符,getchar会阻塞,直到有输入或收到信号。

        

                                                图8-3 getchar函数执行过程

8.5本章小结

本章阐述了Linux“一切皆文件”的IO设备管理思想,介绍了核心的Unix IO接口函数;深入分析了printf和getchar的底层实现流程,揭示了标准IO函数到系统调用、再到硬件执行的完整链路;验证了用户态与核心态的切换、驱动程序的桥梁作用。IO管理通过分层架构和统一接口,隔离了用户程序与硬件细节,实现了不同IO设备的统一、高效访问,是程序与外部环境交互的核心机制。

(第8章 1分)

结论

本报告以Hello程序为研究对象,完整剖析了其“从程序到进程”(P2P)的全生命周期及“从源码到输出”(O2O)的端到端流程,核心结论如下:

  1. 静态处理阶段:Hello程序从hello.c源码开始,经预处理(文本格式化)、编译(高级语言到汇编)、汇编(汇编到机器码)、链接(符号解析与重定位),最终生成可执行文件,各阶段生成的中间文件(hello.i、hello.s、hello.o)分别承担了标准化输入、指令过渡、机器码载体的作用,ELF格式贯穿始终,保障了各工具间的兼容性;
  2. 动态运行阶段:用户执行./hello时,Shell通过fork创建子进程,execve加载程序到新的虚拟地址空间,操作系统调度进程执行,通过段式、页式管理实现地址转换,借助TLB和Cache提升内存访问效率,通过IO操作完成结果输出,进程终止后释放资源;
  3. 核心机制支撑:整个生命周期依赖软硬件协同工作,编译器工具链实现了高级语言到底层指令的转化,操作系统通过进程管理、存储管理、IO管理提供了独立、高效的运行环境,地址转换、写时复制、缺页中断、动态链接等机制优化了资源利用率和执行效率。

通过实验深刻体会到计算机系统“分层架构”的合理性,各层各司其职、屏蔽细节,既降低了设计复杂度,又提升了系统的可扩展性。本次实验将理论知识与实践操作相结合,加深了对计算机系统底层原理的理解,认识到任何高级语言程序的执行,最终都离不开指令、内存、IO的协同工作。

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


附件

hello.c:Hello程序原始C语言源码文件,包含main函数和printf函数调用;

hello.i:预处理后的C语言文件,展开stdio.h头文件,删除注释,无预处理指令;

hello.s:x86-64架构汇编语言文件,包含main函数和printf调用对应的汇编指令;

hello.o:可重定位目标文件,包含机器码、数据及重定位信息;

hello:可执行目标文件,动态链接C标准库,执行后输出“Hello World”

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


参考文献

为完成本次大作业你翻阅的书籍与网站等

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

[2] https://www.cnblogs.com/pianist/p/3315801.html

[3] 段页式访存——逻辑地址到线性地址的转换_movl 8(%ebp), %eax-CSDN博客

[4] 段页式访存——线性地址到物理地址的转换_线性地址转为物理地址例题-CSDN博客

[5] 我把 CPU 三级缓存的秘密,藏在这 8 张图里 - 彭旭锐 - 博客园

[6] 【ARM-MMU】ARMv8-A 的4K页表四级转换(VA -> PA)的过程_arm mmu 粒度为什么是4k-CSDN博客

[7] 动态存储分配_百度百科

[8] 什么是缺页中断(缺页中断详解)-CSDN博客

[9] Linux0.11内核execve函数实现原理与源码解析-开发者社区-阿里云

[10] 标准IO常用函数接口 - Dazz_24 - 博客园

[11] 32 IO管理:Linux如何管理多个外设?

[12] 揭秘C语言printf函数源码:揭秘内部实现,助你掌握打印输出技巧 - 云原生实践

[13] getchar函数详解看这一篇就够了-C语言(函数功能、使用、返回值)-CSDN博客

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

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

原文链接:https://blog.csdn.net/2403_86517790/article/details/156492109

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

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