关注

程序人生-Hello‘s P2P

摘  要

Hello 是全世界大多数程序员编写的第一个程序。虽然程序员们呢在编写、运行完这个程序后很快便会将其遗忘,投入到其他难度更高的程序中去,但这个短小的hello程序,却包含着一个程序运行的所有流程,我们可以见微知著地了解到计算机科学的思想精华。

本文通过跟踪hello的一生,介绍hello从编写到运行结束的过程,对计算机底层进行分析。

关键词:计算机系统;C语言;Linux;                            

目  录

第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的P2P,020的整个过程。

gcc通过调用cpp、cs1、as、ld等,在编译过程中对hello程序源文件进行预处理、编译、汇编、链接,最后得到可执行目标文件hello,保存在磁盘中。运行可执行文件hello,shell调用fork创建子进程,并使用execve函数将hello载入并创建运行环境,如为其分配虚拟空间,逐渐将hello载入物理内存,通过操作系统提供的异常控制流等工具,不断对系统中运行的进程进行调度。Unix I/O提供了hello与程序员和系统文件交互的方式。,当main中的循环结束后,程序从中返回,代表程序的终止。这之后,为了释放hello占据的资源,作为其父进程的shell会将其回收,操作系统内核删除相关数据结构。这样,hello的一生到此结束。

1.2 环境与工具

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

1.2.1 硬件环境

处理器:13th Gen Intel(R) Core(TM) i7-13650HX (2.60 GHz)

RAM:32.00GB

1.2.2 软件环境

Windos11 64位;Ubuntu

1.2.3 开发、调试工具

gcc/vim/GDB/EDB/readelf

1.3 中间结果

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

hello.i:预处理后文件

hello.s:编译之后的汇编文件

hello.o:汇编之后的可重定位目标文件

hello:链接之后的可执行目标文件

elf.txt:hello.o的elf格式

hello_objdump.s:hello的反汇编

d_hello.txt:hello的反汇编代码

1.4 本章小结

本章对hello的一生,即hello的运行过程进行了简要的介绍,描述了hello的P2P、020过程,列出了使用的相关工具和文中使用的中间文件。


第2章 预处理

2.1 预处理的概念与作用

程序预处理是编译过程的第一个阶段,可以理解为编译器在正式编译代码前对源代码进行的文本整理和加工。预处理只按照预设规则对代码文本进行替换,删除,插入等操作,处理后代码交给编译器的下一个阶段。具体而言,预处理阶段会将#include<>中引用的库替换为头文件中的实际代码,将所有定义的变量替换为定义的值,并删除所有的注释。

预处理的核心作用是通过代码复用与模块化、宏定义与常量替换、条件编译以及其他辅助功能来实现简化代码编写、提升代码灵活性、可维护性的目的。

2.2在Ubuntu下预处理的命令

图1.ubuntu下预处理命令

图2.生成预处理文件

2.3 Hello的预处理结果解析

打开hello.i查看文件:

开头时引入外部.h文件;

Typedef进行数据类型名称转换;

引入外部函数;

最后部分为main函数源代码。

2.4 本章小结

本章介绍了hello的预处理过程,简要分析了预处理生成文件hello.i,可发现,源代码仅有23行,经处理后产生了长为3061行的文件。若使用3000行来编写一个简单的hello程序,无疑效率是极为低下的。由此,可看出预处理的意义:省去复杂、低效率的编写过程,让我们能使用简洁、可读性高、便于修改、利于调试的代码。


第3章 编译

3.1 编译的概念与作用

编译过程中,编译器将预处理后的代码按照语法规则和目标CPU的指令集规则,翻译成等价的汇编语言文件(.s文件)。编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析,语法分析,语义检查和中间代码生成,代码优化,目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。本阶段编译会执行语法 / 语义分析 + 代码优化 + 指令映射,是真正把 “高级逻辑” 翻译成 “硬件能理解的低级逻辑” 的第一步。

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

        

3.2 在Ubuntu下编译的命令

编译命令:gcc -S hello.i -o hello.s

应截图,展示编译过程!

3.3 Hello的编译结果解析

编译生成了hello.s,为汇编代码,打开hello.s:

首先,前8行包括对string常量的定义,

汇编文件在LC0和LC1存放了两个字符串,一个是printf("用法: Hello 2024112788 曲建衡 13796050819 2!\n");函数中的字符串;另一个是for循环中的字符串。一个汉字对应一个\xxx,他们被存放在rodata节,意为只读数据。

if(argc!=5)语句中的常量被存储在.text中,作为指令的一部分。同理,循环for(i=0;i<10;i++)中的常量0,10也被存储在.text节。

此外,.Data节中存放了已经初始化并且不为0的全局变量。不过,这个节的初始化不需要汇编语句,而是通过虚拟内存请求二进制零的页完成。

局部变量在.text中进行存储,以main中定义的int i为例:

表示整型变量i存储于栈中%rbp-4的位置。

算术操作(如for循环代码中使用的++)在汇编代码中也有体现。

其对应的汇编代码为:对i进行自加,栈上存储的变量i的值+1.

关系操作和控制转移,如if(argc!=5)语句,其汇编代码位于:

Je用于判断cmpl产生的条件码,若两个操作数的值不相等,则跳过本该执行的语句;(在本程序中为第一个printf)。

For循环中的执行条件,汇编代码为:

这里采取了init – 跳转到中间 – 循环判断的模式。jle用于判断cmpl产生的条件码,若后一个操作数的值小于等于前一个,则跳转到.L4——重新执行循环;

数组、指针、结构操作:int main(int argc,char *argv[]),main的参数中有指针数组char *argv[],在该数组中,argv[0]指向输入程序的路径和名称,其他分别表示一个字符串。

Char*类型占据8个字节,根据

以及

对比原函数,可知通过M[%rbp-32+16]和M[%rbp-32+8],分别得到argv[1]和argv[2]两个字符串的首地址。

函数操作方面:

Main函数汇编代码为:

参数传递:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储。

函数调用:被系统启动函数调用。

函数返回:设置%eax为0并且返回,对应return 0 。

Printf函数汇编代码为:

参数传递:

传入了格式字符串的地址、 argv[1]、argc[2]的地址。

函数调用:

在for循环的过程中调用。

源代码为:

printf("Hello %s %s %s\n",argv[1],argv[2],argv[3]);

3.4 本章小结

本章介绍了编译的概念以及过程。通过hello函数分析了c语言如何转换成为汇编代码。介绍了汇编代码如何实现变量、常量、传递参数以及分支和循环。


第4章 汇编

4.1 汇编的概念与作用

把汇编语言翻译成机器语言的过程称为汇编,汇编器同时将汇编程序(.s文件)打包成可重定位目标程序(.o文件)。这里的.o是二进制文件,而.s仍然是文本文件。

通过汇编,汇编代码转化为了计算机能够完全理解的机器代码。

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

4.2 在Ubuntu下汇编的命令

应截图,展示汇编过程!

4.3 可重定位目标elf格式

生成elf的命令:

Elf文件中包含如下信息:

  1. ELF头:

Elf头包含了系统信息,编码方式,ELF头大小,节的大小和数量等等一系列信息,内容如上。

  1. 节头:

节头目表描述了.o文件中出现的节的类型、位置、所占空间大小信息。

  1. 重定位节:

每个段所引用的外部符号等在进行链接时会需要通过重定位对这些位置的地址进行修改,链接器会通过重定位的推奥姆计算出正确的地址。

hello.o需要对以下符号进行重定位:rodata中的模式串,puts,printf等。

      2.符号表:

符号表symbol table是存放在程序中用于定义和引用函数全局变量的信息。

4.4 Hello.o的结果解析

由于直接使用命令:objdump -d -r hello.o产生的反汇编位于命令提示行中,不便于查找,因此使用命令:objdump -d -r hello.o > d_hello.txt,生成txt文件。

数值表示上,hello.s 中操作数以十进制呈现,而 hello.o 反汇编代码的操作数为十六进制。这体现了机器语言与汇编语言的映射关系:汇编以人类易读的十进制展示操作数,机器语言本质是二进制,反汇编时以十六进制呈现,二者数值等价,该差异在分支转移、函数调用等操作中更突出。

控制转移方面,hello.s 通过.L2、.LC1 等段标签跳转,hello.o 反汇编代码则以虚拟地址跳转,但地址暂为 0,仅保留重定位条目,链接阶段会填充正确地址。

函数调用时,hello.s 直接 call 函数名,hello.o 反汇编代码中 call 指向全 0 的虚拟地址并留存重定位条目,链接后才会确定函数实际执行地址。

4.5 本章小结

本章介绍了汇编。经过汇编器,汇编语言转化为机器语言,hello.s文件转化为hello.o可重定位目标文件。我们研究了可重定位目标文件elf格式,利用反汇编工具查看hello的汇编代码,并在其中分析了readelf命令、elf头、节头部表、重定位节、符号表。并对比了hello.s与hello.o,分析了汇编语言到机器语言的变化。从汇编代码变为机器代码后,这个程序就可以真正被计算机理解。


5链接

5.1 链接的概念与作用

链接是指链接器 将一个或多个目标文件(.o)、系统库文件合并,解析所有未定义的符号(如printf函数),修正重定位条目,最终生成符合操作系统可执行格式的可执行文件(hello)的过程。

链接能够解决未定义符号问题,填充真实内存地址,合并目标文件与库,从而生成可执行文件hello。

注意:这儿的链接是指从 hello.o 到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

使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件

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

使用命令:readelf -h hello,获取hello的elf格式

可看到,节头数量由14个变为27个。

节头部表:

使用命令:readelf -S hello

我们可以发现,节头表中增加了一些动态链接的段。

5.4 hello的虚拟地址空间

使用edb加载hello,data dump窗口可查看加载到虚拟地址中的hello程序。Program headers告诉连接器运行时加载的内容并提供动态链接需要的信息。

程序包含PHDR,INTERP,LOAD,DYNAMIC,NOTE,GNU_STACK,GNU_RELRO几个部分,其中PHDR 保存程序头表。INTERP 指定在程序已经从可执行文件映射到内存之后,必须调用的解释器。LOAD 表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据、程序的目标代码等。DYNAMIC 保存了由动态链接器使用的信息。NOTE 保存辅助信息。GNU_STACK:权限标志,用于标志栈是否是可执行。GNU_RELRO:指定在重定位结束之后哪些内存区域是需要设置只读。

  

5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

使用命令:objdump -d -r hello > hello_objdump.s

新增函数:链接加入了在hello.c中用到的库函数,如put,print,getchar,exit等。

新增节:hello中加入了.init和.plt节,和一些节定义的函数。

函数调用地址:hello实现了调用函数时的重定位,因此在调用函数时调用的地址已经是函数确切的虚拟地址。

控制流跳转地址:Hello实现了调用函数时的重定位,因此在跳转时调用的地址已经是函数确切的虚拟地址。

链接的过程:链接就是链接器(ld)将各个目标文件(各种.o文件)组装在一起,文件中的各个函数段按照一定规则累积在一起。从.o提供的重定位条目将函数调用和控制流跳转的地址填写为最终的地址。

5.6 hello的执行流程

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

使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。

在进行动态链接前,首先进行静态链接,生成部分链接的可执行目标文件 hello。此时共享库中的代码和数据没有被合并到 hello 中。只有在加载 hello 时,动态链接器才对共享目标文件中的相应模块内的代码和数据进行重定位,加载共享库,生成完全链接的可执行目标文件。

查看plt段:

利用代码段和数据段的相对位置不变的原则计算变量的正确地址。而对于库函数,需要plt、got的协作。

plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的就是正确的内存地址。plt就能跳转到正确的区域。

5.8 本章小结

本章研究了链接的过程。通过edb查看hello的虚拟地址空间,对比hello与hello.o的反汇编代码,深入研究了链接的过程中重定位的过程。


6hello进程管理

6.1 进程的概念与作用

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

进程提供给应用程序的关键抽象:一个独立的逻辑控制流,如同程序独占处理器;一个私有的地址空间,如同程序独占内存系统。可以说,如果没有进程,体系如此庞大的计算机不可能设计出来。

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

Shell-bash的核心作用:

  1. 命令解析执行:接收用户输入的命令(如ls、gcc),翻译成内核能识别的系统调用,执行后返回结果;

2. 脚本执行引擎:解释运行.sh格式的 bash 脚本,批量执行一系列命令,实现自动化操作;

3. 环境管理:维护用户环境变量(如PATH、HOME)、别名(alias)等,为命令执行提供上下文。

Shell-bash的流程:

  1. 读取输入:从终端 / 脚本文件读取命令行(如./hello);
  2. 解析与预处理:

去重空格、解析特殊符号(如$、|、>);

展开别名、环境变量(如把$PATH替换为实际路径)、通配符(如*.c展开为所有 c 文件);

  1. 语法分析:检查命令语法是否合法(如括号是否匹配、管道符使用是否正确);
  2. 查找命令:

若为内置命令(如cd、echo),直接由 bash 自身执行;

若为外部命令(如./hello、gcc),通过PATH环境变量查找可执行文件路径;

  1. 执行命令:新建子进程(fork),替换为目标程序(exec),让内核执行hello程序;若有管道(|)、重定向(>),则先搭建 IO 通道再执行;
  2. 返回结果:等待命令执行完成,获取退出状态码(0 表示成功,非 0 为失败),并输出执行结果到终端。

6.3 Hello的fork进程创建过程

1.bash 发起 fork 调用:bash 进程(父进程)向内核发起fork()系统调用,请求复制自身;

2.内核复制进程资源:内核会为新进程(子进程)复制父进程的内存空间、文件描述符、环境变量等资源,生成一个与 bash 几乎完全相同的子进程;

3.分配唯一标识:内核为子进程分配新的 PID(进程 ID),并标记父子进程关系(子进程 PPID 指向 bash 的 PID);

4.返回区分父子进程:fork()会返回两次 —— 给父进程返回子进程的 PID,给子进程返回 0,以此区分父子进程;

5.子进程执行 exec 替换:子进程调用exec()系统调用,将自身的代码 / 数据段替换为hello可执行文件的内容,最终执行hello程序;父进程则等待子进程执行完成(通过wait())。

6.4 Hello的execve过程

execve是子进程(由 bash fork 生成)替换自身为hello程序的核心系统调用,核心步骤如下:

1.子进程发起调用:fork 生成的 bash 子进程调用execve("./hello", argv, envp),传入hello路径、命令行参数、环境变量;

2.内核校验文件:内核验证hello是合法的 ELF 可执行文件,且当前用户有执行权限;

3.释放旧资源:子进程清空自身原有的代码段、数据段、堆栈等(保留 PID、文件描述符等核心资源);

4.加载新程序:内核读取hello的 ELF 格式,将其代码段、数据段加载到子进程内存空间,设置程序入口地址(如_start);

5.启动执行:内核跳转到hello的入口地址,开始执行hello的机器码,子进程从此完全变为hello进程。

6.5 Hello的进程执行

进程的运行本质上是CPU不断从程序计数器 PC 指示的地址处取出指令并执行,值的序列叫做逻辑控制流。操作系统会对进程的运行进行调度,执行进程A->上下文切换->执行进程B->上下文切换->执行进程A->… 如此循环往复。 在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。在一个程序被调运行开始到被另一个进程打断,中间的时间就是运行的时间片。

6.6 hello的异常与信号处理

异常类型

触发的信号

触发场景(hello 为例)

内存访问违规

SIGSEGV(11)

hello 访问未授权内存(如数组越界、空指针解引用)

非法指令执行

SIGILL(4)

hello 包含 CPU 不支持的机器码(如编译架构不匹配)

浮点运算错误

SIGFPE(8)

hello 出现除 0、浮点溢出等运算错误

程序正常退出

SIGCHLD(17)

hello 执行完调用exit(),内核向父进程(bash)发该信号

终端中断(手动终止)

SIGINT(2)

执行./hello时按Ctrl+C,内核向 hello 发该信号

段错误(无效内存引用)

SIGBUS(7)

hello 访问内存地址未对齐(如 ARM 架构下非法内存访问)

操作 / 命令

触发信号

进程状态变化

核心处理逻辑

乱按 + 回车

运行态(正常)

纯 IO 操作,内核转发输入,无信号干预

Ctrl+C

SIGINT(2)

运行态→终止

默认终止进程,shell 回收终端

Ctrl+Z

SIGTSTP(20)

运行态→停止态

暂停进程,保留上下文,释放 CPU

jobs

无变化

shell 查询终端作业列表

ps

无变化

查询系统进程状态(T = 停止、R = 运行)

pstree

无变化

展示进程父子关系

fg

SIGCONT(18)

停止态→运行态(前台)

恢复进程运行,重新分配 CPU 时间片

kill PID

SIGTERM(15)

停止 / 运行态→终止

优雅终止,进程可自定义处理

kill -9

SIGKILL(9)

停止 / 运行态→终止

强制终止,不可捕获 / 忽略

kill -STOP

SIGSTOP(19)

运行态→停止态

强制暂停,不可捕获 / 忽略

6.7本章小结

本章了解了hello进程的执行过程。在hello运行过程中,内核对其调度,异常处理程序为其将处理各种异常。每种信号都有不同的处理机制,对不同的shell命令,hello也有不同的响应结果。


7hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:hello程序编译 / 汇编阶段生成的地址(如.text段起始地址0x8048000),由 “段选择符:偏移量” 组成(如cs:0x8048000),是 CPU 执行指令时看到的原始地址,仅存在于用户态程序中。

线性地址:逻辑地址经段式管理转换后的一维连续地址(如0x8048000),也叫虚拟地址的中间形态,x86_64 下逻辑地址几乎等价于线性地址(段基址为 0)。

虚拟地址:hello进程运行时看到的内存地址(如访问变量a的地址0x555555554000),每个进程独享 0~4GB(32 位)/0~2^64(64 位)的虚拟地址空间,与物理内存解耦。

物理地址:实际硬件内存的地址(如0x12340000),由虚拟地址经页式管理转换而来,是 CPU 访问内存芯片的真实地址,所有进程共享物理地址空间。

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

核心逻辑:x86 架构通过段描述符表(GDT/LDT) 完成转换,hello的逻辑地址(如cs:0x8048000)中,cs是段选择符,指向 GDT 中代码段描述符;

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

hello实例:Linux 下为简化管理,将所有段的基址设为 0,因此hello的逻辑地址偏移量直接等于线性地址(如逻辑地址cs:0x8048000→线性地址0x8048000),段式管理仅做权限检查(如代码段只读),无实际地址偏移

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

核心逻辑:内核将线性地址(虚拟地址)按页大小(4KB)划分为页目录、页表、页偏移,通过页表映射到物理页框;

hello实例:访问hello的main函数虚拟地址0x555555554000时,内核先查页目录找到页表项,再查页表得到物理页框号,最后加上页偏移(如0x000),得到物理地址0x12340000;

关键:hello的虚拟地址空间与物理地址空间无直接关联,相同虚拟地址可映射到不同物理地址。

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

四级页表:64 位 Linux 下,hello的虚拟地址分为页全局目录(PGD)、页上级目录(PUD)、页中间目录(PMD)、页表(PTE)、页偏移 5 部分,逐级查找最终定位物理页框;

TLB(快表):CPU 内置的页表缓存,存储hello最近访问的 VA→PA 映射关系;

hello实例:访问hello的变量地址时,先查 TLB,命中则直接得到物理地址(纳秒级);未命中则遍历四级页表(耗时更长),并将结果写入 TLB,提升后续访问效率。

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

三级 Cache(L1/L2/L3)是 CPU 与物理内存间的高速缓存,按 “局部性原理” 缓存hello的指令 / 数据;

hello实例:CPU 需读取hello的printf指令物理地址时,先查 L1 Cache(CPU 核心内),命中则直接执行;未命中查 L2(CPU 核心外),再未命中查 L3,最后访问物理内存(DRAM);

核心:Cache 缓存hello频繁访问的代码 / 数据,将内存访问延迟从百纳秒级降至纳秒级,提升hello运行速度。

7.6 hello进程fork时的内存映射

fork创建hello子进程时,内核采用写时复制(COW) 机制:

初始:子进程共享hello父进程的虚拟地址空间和物理页框,页表标记为 “只读”;

写操作:若子进程修改变量(如a=10),内核为该页分配新物理页框,复制数据后修改页表映射,子进程独享该页,父进程不受影响;

核心:避免 fork 时全量复制内存,节省资源,仅在写入时分配新页。

7.7 hello进程execve时的内存映射

execve替换bash子进程为hello时,内核重新构建虚拟地址空间:

清空原有虚拟地址映射(保留 PID、文件描述符);

解析hello的 ELF 文件,将.text(代码)、.data(数据)段映射到虚拟地址(如0x555555554000);

为堆、栈分配虚拟地址空间,设置动态库(如 libc)的映射地址;

程序计数器指向hello的入口地址(_start),完成映射。

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

缺页故障:hello访问虚拟地址时,若该地址未映射到物理页框(如首次访问堆内存、动态库代码),触发缺页异常;

处理流程:

CPU 从用户态切到核心态,内核捕获缺页中断;

若为合法地址(如hello的堆扩展):分配物理页框,建立页表映射,加载数据(如从磁盘读动态库),返回用户态继续执行;

若为非法地址(如空指针访问):内核发送SIGSEGV信号,hello触发段错误终止。

7.9动态存储分配管理

Printf会调用malloc,请简述动态内存管理的基本方法与策略。(此节课堂没有讲授,选做,不算分)

7.10本章小结

hello进程的内存管理与地址转换围绕虚拟地址与物理地址解耦展开:逻辑地址经段式管理(Linux 下简化为直接映射)转为线性地址,再通过页式管理 + TLB 加速映射到物理地址,三级 Cache 进一步提升物理内存访问效率;fork时以写时复制机制共享内存,execve则重建虚拟地址空间映射 ELF 文件;访问未映射地址触发缺页中断,合法地址分配物理页并映射,非法地址则触发段错误终止进程。


8hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

Linux 采用 “设备文件化” 模型管理 IO 设备,即将所有硬件设备(如键盘、显示器、磁盘)抽象为文件,统一纳入文件系统管理。设备管理的核心是通过Unix IO 接口实现对设备的操作,无需区分设备类型,均以文件读写的方式交互,实现了 “设备无关性”—— 例如操作显示器(字符设备)和磁盘文件的接口完全一致。

8.2 简述Unix IO接口及其函数

Unix IO 接口是一套标准化的系统调用函数,为设备操作和文件操作提供统一接口,核心函数包括:

open():打开设备文件或普通文件,返回文件描述符(FD),用于后续操作;

read():从文件描述符对应的设备 / 文件中读取数据(如从键盘读按键、从磁盘读内容);

write():向文件描述符对应的设备 / 文件写入数据(如向显示器输出字符串、向磁盘写内容);

close():关闭文件描述符,释放相关资源;

ioctl():用于设备的特殊控制操作(如设置串口波特率)。这些函数屏蔽了底层设备差异,用户程序通过文件描述符即可完成对不同设备的操作。

8.3 printf的实现分析

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

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

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

vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

printf 的实现围绕 “格式化处理 - 系统调用 - 驱动显示” 展开,完整流程如下:

格式化数据:printf 通过va_list解析可变参数(...),调用vsprintf函数将格式化字符串(如"Hello %d")与参数结合,生成标准化的 ASCII 字符串,存入缓冲区;

发起系统调用:调用write()系统函数,传入文件描述符 1(对应显示器设备)、缓冲区地址及字符串长度,通过int 0x80(32 位)或syscall(64 位)陷阱指令触发用户态到核心态的切换;

内核处理与驱动调用:内核通过系统调用表定位sys_write函数,将缓冲区数据传递给字符显示驱动;

字模转换与 VRAM 写入:显示驱动将 ASCII 码映射为对应的字符字模(像素点分布信息),并写入显示内存(VRAM);

硬件显示:显示芯片按固定刷新频率逐行读取 VRAM 中的 RGB 颜色数据,通过信号线传输至显示器,最终呈现字符。

8.4 getchar的实现分析

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

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

getchar 的实现依赖 “键盘中断 - 缓冲区 - 系统调用” 的协同机制,流程如下:

键盘中断触发:用户按下键盘时,键盘控制器产生异步中断信号,CPU 切换至核心态执行键盘中断处理子程序;

扫描码转 ASCII 码:中断子程序读取键盘扫描码(硬件编码),转换为程序可识别的 ASCII 码,存入内核的键盘缓冲区;

系统调用请求:getchar 底层调用read(0, buf, 1)(文件描述符 0 对应键盘)发起系统调用,若缓冲区无数据,程序进入阻塞状态;

数据读取与返回:内核从键盘缓冲区提取首个 ASCII 码,传入用户程序缓冲区buf;

完成输入:getchar 从buf获取字符并返回,若用户按下回车键,read()会一次性返回缓冲区中所有字符,结束本次输入。

8.5本章小结

本章核心围绕 Linux “设备文件化” 的 IO 管理思想展开:通过 Unix IO 接口实现设备与文件的统一操作,屏蔽底层硬件差异,降低开发复杂度。printf 通过格式化处理、系统调用及显示驱动,完成字符的显示器输出;getchar 借助键盘中断、缓冲区缓存及read系统调用,实现按键输入的读取。整个 IO 流程遵循 “用户态调用 - 核心态处理 - 驱动控硬件” 的逻辑,既保证了操作的统一性,又通过内核隔离保障了系统安全性。

结论

经过上述流程,我们已经了解了hello程序从源代码到运行结束的完整生命流程。以下为对每个流程的总结:

  1. 源文件hello.c经过预处理转换为预处理文件hello.i;
  2. 预处理文件hello.i经过编译器处理转换为汇编语言代码hello.s;
  3. 汇编语言代码hello.s经过汇编器生成包含代码段、数据段、重定位信息的目标文件hello.o;
  4. Hello.o经过连接器与系统库合并,解析未定义符号(如printf等),修正重定位符号,生成可执行文件hello;
  5. 可执行程序hello经过shell调用fork()创建进程,创建bash的子进程,分配pid;
  6. 子进程调用execve()系统调用,内核清空原有内存映射,解析hello的 ELF 文件,将.text、.data段映射到虚拟地址空间,初始化堆、栈,设置程序计数器指向_start入口地址;
  7. 内核调度器调度hello进程,CPU 经地址转换(段式 / 页式 + TLB 加速)访问物理内存,Cache 缓存指令 / 数据提升效率;
  8. printf格式化字符串后,通过write()系统调用触发特权级切换,经显示驱动将数据写入 VRAM,驱动显示器输出;
  9. 键盘按键触发中断,扫描码转 ASCII 码存入内核缓冲区,getchar通过read()系统调用阻塞读取数据;
  10. 接收信号(如 Ctrl+C/Ctrl+Z)按默认或自定义规则响应;非法操作触发缺页中断,合法则分配物理页,非法则终止进程
  11.  hello执行exit(),内核释放资源并向父进程发信号,父进程回收退出状态,进程生命周期结束。


附件

hello.i:预处理后文件

hello.s:编译之后的汇编文件

hello.o:汇编之后的可重定位目标文件

hello:链接之后的可执行目标文件

elf.txt:hello.o的elf格式

hello_objdump.s:hello的反汇编

d_hello.txt: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.

[7]  C语言编译全过程介绍 – 百度文库.https://wenku.baidu.com/view/8976aeb765ce05087632130a.html?_wkts_=1767430019661

[8]  linux下的文件I/O编程https://www.linuxprobe.com/linux-file-i-o.html

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

原文链接:https://blog.csdn.net/2501_90852890/article/details/156541718

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

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