关注

程序人生-Hello’s P2P

计算机系统

大作业

题     目  程序人生-Hellos P2P  

专       业    计算学部            

学     号    2023112936          

班   级    23L0513             

学       生    张可          

指 导 教 师    史先俊          

计算机科学与技术学院

20255

摘  要

本文以经典的hello程序为切入点,深入探究了其在Linux系统中的完整运行轨迹。这个看似简单的程序实际上串联起了计算机系统的多个核心模块:首先经历预处理器的宏展开、编译器的语法分析与优化、链接器的符号解析等编译过程;随后在运行时通过fork/exec等系统调用完成进程创建,并由虚拟内存管理系统构建地址空间;最终在CPU调度执行和存储体系结构的支持下完成输出功能。

通过对该程序生命周期的完整追踪,我们得以窥见现代操作系统的三个关键技术支柱:编译系统的高效代码转换能力、存储管理单元的多级地址转换机制,以及进程管理模块的资源隔离与调度策略。这生动展现了计算机系统各层抽象的协作关系。

关键词:hello;计算机系统;程序运行过程分析;编译过程                         

目  录

第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简介

1. 1.1 P2P(Program to Process)过程

(1)程序存储(Program):Hello以可执行文件形式存储在磁盘上(如hello.c编译为hello),包含机器指令、数据段、符号表等元数据。

(2)进程创建(Process):用户通过Shell输入./hello,Shell调用fork()创建子进程,子进程通过execve()加载Hello的代码段和数据段到内存,形成进程映像。操作系统为进程分配PID、虚拟地址空间、文件描述符表等资源,并初始化寄存器(如PC指向main入口)。

(3)进程执行:CPU按指令周期执行Hello的代码:访问.data段中的字符串"Hello, world",调用write()系统调用,通过内核缓冲区将输出传递到终端设备。

(4)进程终止:Hello执行完毕后调用exit(),释放内存、关闭文件描述符,父进程Shell通过wait()回收其退出状态(Exit Status),内核清除进程描述符(PCB)。

1.1.2 O2O(Zero to Zero)过程

(1)从零开始(Zero)

源码阶段:Hello作为文本文件(hello.c)存在,尚未被编译器处理,属于“零状态”的源代码形式。

(2)生成可执行文件(Object)

·预处理:cpp处理宏和头文件,生成扩展的hello.i。

·编译:cc1将源码转为汇编(hello.s),再经as生成目标文件hello.o(包含ELF格式的机器码)。

·链接:ld合并hello.o与libc.so等库,解析符号引用,生成可执行的ELF文件hello。

(3)运行后归零(Zero):进程终止后,所有运行时资源(堆栈、打开的文件、CPU上下文)被内核回收,内存页被标记为可重用,进程状态回归“零”(系统初始态)。

1.2 环境与工具

硬件环境:

CPU: Intel Core i7-12700H (14核20线程, 2.5GHz~4.7GHz)  

内存: 16GB DDR5 (可扩展)  

显卡: NVIDIA RTX 3060/3070 移动版 + Intel Iris Xe 核显  

主板: LENOVO 82RF (定制版)  

存储: 1TB NVMe SSD

操作系统: Windows 11 家庭中文版 64位

软件环境:DELL服务器

开发与调试工具:Visual Studio;objdump,gcc,gdb,readelf等工具

1.3 中间结果

文件名

作用

hello.c

原始C程序代码。

hello.i

预处理后得到的文本文件。

hello.s

目标平台汇编代码。

hello.o

包含机器码和符号表的可重定位文件。

hello

可执行文件。

hello.elf

用readelf读取出的hello.o的ELF格式信息。

1.4 本章小结

本章首先通过分析hello的自白分析了P2P和O2O的过程,然后说明了实验中所用到的环境与工具,最后对于实验中间生成文件及其作用进行了阐述。


第2章 预处理

2.1 预处理的概念与作用

2.1.1 预处理概念:预处理是编译过程的第一个阶段,由预处理器(如cpp)对源代码进行文本级别的处理。它在实际编译之前执行,通过解析以#开头的指令(如#include、#define),直接修改源代码文本,生成一个纯C代码的中间文件(如.i文件),不涉及语法分析或机器码转换。

2.1.2 预处理作用:预处理的核心任务是简化后续编译阶段的输入,具体包括:

(1)宏展开:将#define定义的常量或宏替换为实际值或代码片段。

(2)头文件包含:递归插入#include指定的头文件内容,合并到源文件中。

(3)条件编译:根据#ifdef、#if等指令保留或排除特定代码块(如平台适配代码)。

(4)删除注释:移除所有注释,减少编译无关内容。

2.2在Ubuntu下预处理的命令

命令:gcc -E -m64 -no-pie -fno-PIC hello.c -o hello.i

图1 hello.i的生成

2.3 Hello的预处理结果解析

图2 hello.i部分代码展示

可以发现,在预处理后生成的hello.i文件中,预处理将hello.c转换为自包含的纯C代码文件:

(1)移除所有注释和预处理指令。(例如将hello.c中头部的注释内容删除)

(2)展开头文件,显式声明所有依赖的外部函数。(例如在extern int printf (const char *__restrict __format, ...)以及extern void exit (int __status)段中,即插入标准I/O函数printf、exit的声明)

(3)保留用户代码逻辑不变,确保后续编译阶段可直接处理。

2.4 本章小结

本章首先介绍了预处理的概念以及作用,其次在Linux下将原hello.c文件进行预处理生成了hello.i文件,最后通过分析hello.i文件对预处理的效果进行了一个分析与说明。


第3章 编译

3.1 编译的概念与作用

3.1.1 编译的概念:编译指将预处理后的中间文件(.i)转换为目标架构特定的汇编代码(.s)的过程,由编译器前端(如gcc的cc1)完成。此阶段进行词法分析、语法分析、语义分析,生成抽象语法树(AST),最终翻译为与硬件平台相关的汇编指令集(如x86、ARM等)。

3.1.2 编译的作用:将高级C代码降级为低级汇编代码,保留逻辑等价性。验证类型、作用域等语义规则(如未声明变量的报错)。执行常量传播、死代码删除等基础优化,提升汇编效率。根据目标CPU指令集(如-m64指定x86-64)生成对应汇编。  

3.2 在Ubuntu下编译的命令

命令:gcc -S -m64 -no-pie -fno-PIC hello.i -o hello.s

图3 hello.s的生成

3.3 Hello的编译结果解析

3.3.1 数据与变量分析

(1)常量

·字符串常量

图4 字符串常量

.LC0:

    .string "\347\224\250\346\263\225: Hello \345\255\246\345\217\267..."  ; UTF-8编码的中文字符串

.LC1:

    .string "Hello %s %s %s\n"  ; 格式化字符串

存储在 .rodata 只读数据段,通过标签(.LC0, .LC1)引用。

·整型常量

图4 整形常量

movl    $0, -4(%rbp)           ; 数字0(初始化i)

(2)变量

·局部变量 i

图5 局部变量i

movl    $0, -4(%rbp)           ; int i = 0(图4)

addl    $1, -4(%rbp)           ; i++

存储在栈帧偏移 -4(%rbp) 处,4字节整型。

3.3.2 赋值操作

(1)直接赋值

movl    $0, -4(%rbp)           ; i = 0(图4)

(2)指针解引用赋值

图6 指针解引用赋值

movq    -32(%rbp), %rax        ; rax = argv

addq    $8, %rax               ; rax += 8 (argv[1]地址)

movq    (%rax), %rax          

movq %rax, %rsi              ; rsi= *rax (argv[1]值)

3.3.3 类型转换

·字符串→整型(atoi)

图7 字符串→整形

movq    (%rax), %rdi           ; argv[4]字符串地址

call    atoi                   ; atoi将字符串转为整形,返回值在%eax

movl    %eax, %edi             ; 传递给sleep

3.3.4  sizeof

3.3.5 算术运算

·加法(+=、++)

图8 加法

addq    $8, %rax               ; 指针算术(rax += 8)

addl    $1, -4(%rbp)           ; i++

3.3.6 逻辑/位操作

·逻辑操作(!)

在3.3.9(1)(图10)中,实现了:!(argc==5)-->执行puts

3.3.7 关系操作

·条件判断(!=)

图9 条件判断

cmpl    $5, -20(%rbp)          ; argc != 5

je      .L2                    ; 跳转

3.3.8 数组/指针/结构操作

(1)数组操作

在3.2.2(2)(图6)中实现了基址偏移+解引用的数组操作

(2)指针操作

指针解引用操作:

图10 指针操作

movq    (%rax), %rdx          ; rdx = *rax (解引用指针)

3.3.9 控制流分析

(1)条件分支(if)

图11 if

cmpl    $5, -20(%rbp)          ; 比较argc和5

je      .L2                     ; 相等跳转

movl    $.LC0, %edi

call    puts                   ; 不相等时执行puts

movl    $1, %edi

call    exit                    ; 退出

(2)循环(for)

图12 for

.L2:

movl    $0, -4(%rbp)           ; i = 0

jmp     .L3

.L4:                          ; 循环体...

addl    $1, -4(%rbp)           ; i++

.L3:

cmpl    $9, -4(%rbp)           ; i <= 9

jle     .L4                    ; 继续循环

3.3.10 函数调用分析

(1)main调用

图13 main

参数为argc值,argv地址。

  1. puts调用

图14 puts

  1. exit调用

图15 exit

  1. printf调用

图16 printf

调用参数argv[1],argv[2]。该函数调用了两次(总的为十次)。第一次将寄存器%rdi设置为待传递字符串"用法:Hello学号 姓名 秒数!\n"的起始地址;第二次将其设置为“Hello %s  %s\n”的起始地址。使用寄存器%rsi完成对argv[1]的传递,用%rdx完成对argv[2]的传递。                        

  1. atoi调用

图17 atoi

atoi函数将参数argv[3]放入寄存器%rdi中用作参数传递,简单使用call指令调用。atoi函数用于将字符串转换为整数。

  1. sleep调用

图18 sleep

代码将转换完成的秒数从%eax传递到%edi中,edi存放sleep的参数,再使用call调用。Sleep函数可以让程序休眠一段时间。

  1. getchar调用

图19 getchar

3.4 本章小结

本章首先介绍了编译的概念以及作用,然后在Linux下将hello.i文件进行编译生成了hello.s文件,然后结合hello.s的内容,在参考C语言的数据与操作的情况下探讨了各种数据与操作在hello.s中的体现,并且通过比较具体阐述了其实现方法。


第4章 汇编

4.1 汇编的概念与作用

4.1.1 汇编的概念:汇编(Assembling)是将汇编代码(.s文件)转换为机器语言二进制目标文件(.o文件)的过程,由汇编器(如as)完成。该阶段将人类可读的汇编指令(如movl, call)逐条翻译为CPU可直接执行的机器码(二进制编码),并生成可重定位的目标文件(ELF格式),包含代码段(.text)、数据段(.data)和符号表等元数据。

4.1.2 汇编的作用:将助记符(如addq)转换为二进制操作码(如0x4801C3)。标记未确定的符号引用(如printf),留待链接阶段处理。输出ELF格式的.o文件,包含机器码、重定位信息和调试符号(若有-g选项)。确保指令集匹配目标CPU(如x86-64的movq对应64位操作)。

4.2 在Ubuntu下汇编的命令

命令:gcc -c -m64 -no-pie -fno-PIC hello.s -o hello.o

图20 hello.o的生成

4.3 可重定位目标elf格式

使用readelf -a hello.o > hello.elf来生成elf格式

图21 hello.elf

4.3.1 ELF头表

图22 ELF头表

ELF头表位于文件起始位置,包含16字节Magic Number、文件类型(如REL)、目标架构(如x86-64)、节头表位置等元信息。其标识ELF文件格式,指明是可重定位/可执行文件,定义机器字节序和位宽,为后续节区解析提供导航。

4.3.2节头

图23 节头表

节头表由多个条目组成,每个条目描述一个节区(如.text、.rodata)的名称、类型、偏移量、大小、读写权限等属性。其充当“目录”角色,定位各节区在文件中的物理位置和功能,指导链接器/加载器按需处理代码、数据或调试信息。

4.3.3 重定位节表

图24 重定位节表

重定位节表包含一系列重定位条目,每条记录需修正的指令偏移量、符号引用、修正类型(如R_X86_64_PLT32)及附加值。其标记目标文件中未确定的符号引用(如外部函数printf),供链接器替换为真实地址,是静态链接的核心依据。

4.3.4 符号表

图25 符号表

符号表列出所有符号(函数、变量等),包括符号名、类型(如FUNC)、绑定属性(如GLOBAL)、所属节区(如.text或UND)。其建立符号名到地址的映射,区分已定义符号(如main)和未定义外部符号(如puts),解决跨模块引用问题。

4.4 Hello.o的结果解析

objdump -d -r hello.o  分析hello.o的反汇编,并与hello.s进行对照分析。

与hello.s对比:

(1)指令结构上,机器语言由操作码(Opcode)和操作数(Operand)组成,以二进制编码表示(反汇编显示为16进制)。

(2)操作数差异:汇编语言使用符号化操作数(如puts、.LC0)。而在机器语言中,立即数使用直接编码(如mov $0x1,%edi → bf 01 00 00 00)。地址/符号编码时,先用 00 00 00 00 占位,由重定位条目修正(如 call puts 的偏移量)。寄存器的编码为固定编号(如 %edi → 0x7)。

图26 操作数差异

(3)在函数调用上,在汇编语言中,call后接的是函数名。而在反汇编码中,call后面不再是函数名称,而是下一条指令的地址,且在链接时根据重定位条目修正为函数名的实际地址。如图puts-0x4 表示最终偏移量= puts地址-下条指令地址- 4。

图27 函数调用差异

  1. 在条件跳转中,跳转目标在汇编中用标签(.L4),而在机器码中转换为相对偏移。

图28 条件跳转的差异

4.5 本章小结

在本章中,本章首先介绍了汇编的概念以及作用,然后在Linux下将hello.s文件进行汇编生成了hello.o文件,然后分析了hello.o文件之中各表的内容与其所对应的功能。随后生成了hello.o的反汇编文件,通过对反汇编文件与汇编文件中的内容进行对照,分析出了两者在编码等方面上存在的差异。


5链接

5.1 链接的概念与作用

5.1.1 链接的概念:链接(Linking)是将一个或多个目标文件(hello.o)及库文件合并,解析符号引用并生成可执行文件(hello)的过程。链接器(如ld)负责合并代码段(.text)、数据段(.data等),处理重定位条目(如修正call printf的地址),并添加程序头(如加载信息),最终生成符合操作系统要求的可执行文件格式(如ELF)。

5.1.2 链接的作用:将未定义的符号(如printf)绑定到库中的实现地址。合并所有目标文件的段,确定代码和数据的最终内存布局。根据符号的实际地址修改机器码中的占位值(如call指令的偏移量)。添加运行时所需的元信息(如入口地址、动态库依赖),使程序可被操作系统加载执行。

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

图29 hello的生成

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

5.3.1 ELF头表

图30 ELF头表

分析ELF头信息,可发现hello的EIF格式变为了EXEC。

5.3.2节头表

图31 节头表

节头表中储存了每个节的基本信息,包括名称、类型和地址以及偏移量等,其与每个段之间呈对应关系。

5.3.3 程序头表

图32 程序头表

程序头表之中,存储了每个程序的类型、虚拟地址、偏移量和权限等信息。

5.4 hello的虚拟地址空间

使用gdb对程序进行调试,然后输入set args 学号 姓名 电话号码 秒数,即设置命令行参数,启动gdb后,程序停在main函数入口处,此时通过i proc map显示出虚拟地址空间的布局。

图33 i proc map

此时,为更专注于程序本身的文件映射,方便比较,输入info files以显示被调试程序的内存映射信息。

图34 info files

与之前5.3.2的表对照可发现各段的虚拟地址信息(包括起始与终止)是一致的。

5.5 链接的重定位过程分析

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

·不同在于:

(1)符号地址:从临时到固定。在hello.o中,地址从 0x0 开始,符号地址是临时的,等待链接器分配实际地址;在hello中,地址已分配,符号绑定到实际虚拟地址。

(2)外部函数调用:从重定位到PLT跳转。hello.o中外部函数调用未解析,依赖重定位条目;而在hello中,外部函数通过 PLT(过程链接表) 跳转,实现动态链接。

图35 外部函数调用上的不同

(3) 数据访问:从重定位到绝对地址。hello.o中数据地址未确定,需重定位;而在hello中,数据地址已确定,直接使用虚拟地址。

(4)节数不同:hello.o反汇编中仅有main函数;而hello中加入了_init、.plt等函数和内部程序的反汇编。

·链接过程:

(1) 符号解析(Symbol Resolution):链接器扫描所有目标文件(如 hello.o)和库文件(如 libc.so),将未定义的符号(如 puts)绑定到定义处。

(2) 地址分配(Address Assignment):链接器合并所有 .text、.data 等节,并分配最终虚拟地址。

(3) 重定位(Relocation):根据符号的实际地址,修正机器码中的占位值。

(4) 生成可执行文件:添加程序头(描述如何加载段)、动态段(依赖库列表)等元信息。

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

在链接过程中,链接器会根据 hello.o 的重定位条目对 hello 的机器码进行修正。

例如,在图35中,hello.o 中未解析的 call puts 指令(操作数为 00 00 00 00,对应重定位条目 R_X86_64_PLT32 puts-0x4)会被链接器替换为 call 401090 <puts@plt>,其中401090是 puts 在 PLT(过程链接表)中的入口地址。这些修正使得 hello 中的所有符号引用都绑定到具体的地址,从而生成可独立执行的文件。

5.6 hello的执行流程

通过gdb执行hello,通过在_start、main以及exit等函数处设置断点利用s单步执行、info reg查看寄存器状态以及bt查看栈占用状况hello执行如下:

(1)到_start:_dl_start、_init_first、__init_misc、__strrchr_avx2 、check_stdfiles_vtables

(2)到call main:__libc_start_main_impl、__GI___cxa_atexit、__new_exitfs、__internal_atexit、__libc_start_main_impl、call_init、__libc_start_main_impl、__GI__setjmp、__GI___sigsetjmp、__sigjmp_save、__libc_start_call_main、

(3)程序终止:__run_exit_handlers、___pthread_mutex_lock、lll_mutex_lock_optimized、___pthread_mutex_lock、___pthread_mutex_unlock、__GI___pthread_mutex_unlock_usercnt、___pthread_mutex_unlock、___pthread_mutex_unlock、__GI___pthread_mutex_unlock_usercnt、___pthread_mutex_unlock、__GI___pthread_mutex_unlock_usercnt、lll_mutex_unlock_optimized、__GI___pthread_mutex_unlock_usercnt、_fini、__run_exit_handlers、_IO_cleanup、__GI___libc_cleanup_push_defer、__GI__IO_flush_all、IO_validate_vtable、__GI__IO_flush_all、__GI___libc_cleanup_pop_restore、__GI__IO_flush_all、_IO_cleanup、_IO_unbuffer_all、__GI___libc_cleanup_push_defer、_IO_unbuffer_all、IO_validate_vtable、_IO_unbuffer_all、IO_validate_vtable、_IO_unbuffer_all、_IO_new_file_setbuf、_IO_default_setbuf、IO_validate_vtable、_IO_default_setbuf、_IO_new_file_sync、_IO_default_setbuf、__GI__IO_setb、_IO_default_setbuf、__GI__IO_setb、_IO_default_setbuf、_IO_new_file_setbuf、_IO_unbuffer_all、IO_validate_vtable、_IO_unbuffer_all、_IO_new_file_setbuf、_IO_default_setbuf、IO_validate_vtable、IO_validate_vtable、_IO_default_setbuf、_IO_new_file_sync、_IO_default_setbuf、__GI__IO_setb、_IO_new_file_setbuf、_IO_unbuffer_all、__GI___libc_cleanup_pop_restore、_IO_cleanup、__run_exit_handlers、__GI__exit

5.7 Hello的动态链接分析

·动态链接结构

PLT (Procedure Linkage Table):用于延迟绑定,首次调用函数时跳转到动态链接器解析地址。

GOT (Global Offset Table):存储外部函数的实际地址,首次调用前填充为PLT跳转地址,解析后更新为真实地址。

在gdb中,首先查看printf@plt的地址后,观察其代码段获得此时的GOT地址。

图36 GOT地址

然后,在printf@plt入口处和main设置断点,运行,当运行到printf@plt断点时,再次检查GOT条目。可以发现动态链接器解析 printf 在 libc 中的地址,并更新 GOT。此时跳转的结果更新为libc中printf的真实地址。

图37 验证地址对应的符号

5.8 本章小结

在本章中,本章首先介绍了汇编的概念以及作用,然后在Linux下链接生成了hello文件,然后分析了hello文件之中elf中的内容和信息,除此之外外还探讨了虚拟地址空间、重定位过程、hello的执行流程以及动态连接分析的问题。


6hello进程管理

6.1 进程的概念与作用

6.1.1 进程的概念:进程是操作系统进行 资源分配和调度的基本单位,表示一个正在执行的程序实例。每个进程拥有独立的虚拟地址空间、文件描述符、寄存器状态等资源,由操作系统内核通过进程控制块(PCB)管理。进程是静态程序(如编译后的二进制文件)在运行时动态执行的实体,具备隔离性(一个进程崩溃通常不会影响其他进程)。

6.1.2 进程的作用:为每个程序提供独立的运行环境,防止内存、文件等资源冲突。通过多进程实现多任务并行(如浏览器和音乐播放器同时运行)。通过进程权限限制对系统资源的访问(如用户进程无法直接操作硬件)。操作系统通过调度算法(如时间片轮转)协调多个进程的CPU使用权。

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

6.2.1 Shell-bash的作用:Shell是用户与操作系统内核间的命令行接口,其核心作用包括解析用户输入、管理进程执行及协调输入/输出流。

6.2.2 处理流程:

·读取命令(从终端或脚本获取输入);

·解析分割(按空格、符号拆分,识别管道/重定向);

·执行命令(内置命令直接处理,外部命令通过fork()创建子进程+exec()加载程序);

·返回结果(显示输出或错误,等待下一指令)。

6.3 Hello的fork进程创建过程

当执行 ./hello 时,shell首先通过 fork() 系统调用创建一个与自己完全相同的子进程(包括内存映像、文件描述符等),此时父子进程处于相同状态;接着子进程通过 execve() 系统调用加载 hello 程序的代码段和数据段,替换原有内存空间,形成独立的进程实体,最终由子进程执行 hello 的 main() 函数,而父进程(Shell)则通过 wait() 等待子进程结束并回收其资源。

6.4 Hello的execve过程

execve() 是系统调用中用于加载并执行新程序的核心机制,其过程可概括为:当 Shell 子进程调用 execve() 时,内核首先清空当前进程的代码段、数据段和堆栈(保留 PID 和文件描述符等属性),然后从磁盘读取 hello 的 ELF 文件头,解析其代码段(.text)、数据段(.data/.bss)和动态链接依赖(如 libc.so),将各段映射到进程的虚拟地址空间,最后将 CPU 指令指针(EIP/RIP)跳转到 hello 的入口地址(如 _start 或 main),完成程序的彻底替换。整个过程保持原进程的 PID 和资源句柄,仅替换内存映像,且若加载失败则返回错误而不终止原进程。

6.5 Hello的进程执行

Hello进程的执行过程涉及操作系统底层的进程调度与状态转换:当hello进程被CPU调度器选中执行时,内核会首先通过上下文切换保存当前进程的寄存器状态(如PC、SP等),并恢复hello进程的上下文信息(包括页表、文件描述符等);在时间片(通常10-100ms)内,hello进程在用户态运行其代码(如printf、sleep等),若发生系统调用(如write)、缺页异常或时间片耗尽,则触发陷入内核态,由内核处理中断或重新调度——若时间片到期,hello进程会被置入就绪队列等待下次调度;系统调用完成后,内核通过模式切换返回用户态继续执行,直到进程终止或再次被抢占。整个过程通过用户态/内核态切换(通过软中断/int 0x80/syscall指令)和调度器(如CFS)的优先级计算实现多任务的高效轮转。 

6.6 hello的异常与信号处理

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

6.6.1 异常类型

(1)中断:由硬件设备(如键盘、定时器)或软件指令触发的异步事件,强制CPU暂停当前任务转向处理中断程序。通常不直接产生信号,而是由内核或驱动处理(如键盘中断触发输入事件)。若中断导致错误(如设备故障),可能间接引发 SIGIO 或 SIGPIPE,默认忽略或终止进程。

(2)陷阱:程序主动触发的同步异常,用于实现系统调用或调试。系统调用通过陷阱进入内核,无直接信号。调试陷阱可能触发 SIGTRAP,默认终止进程并生成核心转储,调试器可捕获处理。

(3)故障:可恢复的同步异常(如缺页异常),CPU 重新执行指令后可能继续运行。缺页异常内核自动加载页面,无信号。非法内存访问触发 SIGSEGV,默认终止进程;可自定义处理(如打印错误日志)。栈溢处会触发 SIGSEGV 或 SIGSTKFLT,通常无法恢复。

(4)终止:不可恢复的严重错误(如硬件故障或内核矛盾),强制终止程序。硬件错误(如奇偶校验错误)触发 SIGBUS,默认终止进程。非法指令会触发 SIGILL,默认终止并生成核心转储。双重故障(CPU 无法处理异常)直接触发系统重启(无信号)。

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

6.6.2 异常与信号的处理分析

(1)不停乱按

图38 不停乱按结果

可以看出,此时程序运行并没有受阻。此时终端默认处于 行缓冲模式,输入的字符暂存于终端输入缓冲区,未被程序主动读取,因此对于程序执行无直接影响。

(2)按回车

图39 按回车结果

可以看见,此时程序的运行并未受影响。回车键发送 \n 字符,触发终端缓冲区的行提交,但程序需主动读取输入才会处理,因此并未受影响。

(3)按ctrl-c(SIGINT)

图40 按ctrl-c结果

可以发现,按下ctrl-c后,程序立即终止,Shell 显示 ^C 并返回提示符。ctrl-c向前台进程组 发送 SIGINT 信号,默认行为是终止进程。若程序捕获了SIGINT则执行自定义函数(这里是终止进程)而非退出。

(4)按ctrl-z(SIGTSTP)

图41 按ctrl-z结果

可以发现,按下ctrl-z后,程序挂起,Shell显示 [1]+ Stopped ./hello ...,返回提示符。这是因为ctrl-z发送 SIGTSTP 信号,内核将进程状态置为 T (stopped),并释放 CPU。

(5)按下ctrl-z后:

·执行ps 

图42 ps结果

查看可以发现hello进程并未结束,而是被挂起。

·执行jobs:

图43 jobs结果

jobs 列出当前 Shell 会话管理的 后台或挂起任务。可以发现此时hello为stopped状态并未结束。

·执行pstree:

图44 pstree结果

pstree 以树形结构显示进程关系,而此时在图中可以发现bash——hello,表明 hello 是 bash 的子进程,还未被回收。

·执行fg :

图45 fg结果

执行fg后,打印了hello的命令行之后,挂起的 hello 恢复前台运行,继续输出。fg 向作业发送 SIGCONT 信号,将进程状态从 T 改为 R,并移回前台进程组。因此进程从原暂停点继续执行。

·执行kill:

图46 kill结果

执行kill之后,此时kill发出了SIGTERM信号,kill杀死了当前的hello进程。

6.7本章小结

本章主首先讨论了进程的概念与作用。其次简述了壳Shell-bash的作用与处理流程之后叙述了hello的fork和execve的过程。之后结合进程上下文信息、进程时间片分析了hello进程调度的过程。最终列举出不同的异常以及其产生信号后,以hello为例,通过在运行时输入不同的指令后分析了shell的特点以及说明了异常与信号的处理。


7hello的存储管理

7.1 hello的存储器地址空间

·逻辑地址:由程序直接生成的地址,是代码中使用的内存引用(如变量指针)。在分段机制下,它由段选择符+偏移量组成,程序看到的地址空间是连续的,但实际物理内存可能分散。

·线性地址:通过分段机制将逻辑地址转换后的中间地址。可作为分页机制的输入。

·虚拟地址:在分页机制下,虚拟地址就是线性地址(两者通常等同)。它为每个进程(如 hello)提供独立的地址空间,使得进程认为自己独占内存。操作系统通过页表将其映射到物理地址,实现内存隔离和共享。

·物理地址:最终实际访问内存硬件的地址。由分页机制将虚拟地址转换而来。只有操作系统和硬件知道物理地址,进程无法直接访问。

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

在 Intel x86 体系结构 中,段式管理 负责将 逻辑地址转换为 线性地址。这一过程涉及段寄存器和段描述符表,具体步骤如下:

1. 逻辑地址的组成

逻辑地址采用 段选择符: 偏移量(Offset) 的形式。

段选择符(16位):用于索引 全局描述符表(GDT) 或 局部描述符表(LDT),包含:

·索引:指定段描述符在 GDT/LDT 中的位置。

·TI:0 = GDT,1 = LDT。

·RPL:权限级别(0=内核,3=用户)。

偏移量(32/64位):要访问的段内地址。

2. 段描述符

段描述符存储在 GDT/LDT 中,定义了一个内存段的属性,包括:

·段基址:该段在内存中的起始地址。

·段界限:该段的最大长度(防止越界访问)。

·访问权限:如可读、可写、可执行等。

·DPL:访问该段所需的最低权限。

3. 转换过程:

(1)CPU 根据段选择符(如 CS、DS)找到对应的段描述符:

若 TI=0,从 GDT(全局描述符表) 读取。

若 TI=1,从 LDT(局部描述符表) 读取。

(2)检查权限(DPL ≥ RPL),确保程序有权访问该段。

(3)将段基址(Base)与偏移量(Offset)相加,得到线性地址:线性地址 = 段基址 + 偏移量。(例如,若段基址=0x1000,偏移量=0x200,则线性地址=0x1200)

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

在页式内存管理中,线性地址(虚拟地址)通过页表转换为物理地址:首先将地址拆分为页号和页内偏移,用页号查询页表获取对应的物理页框号,再将其与偏移量拼接形成物理地址。若页表项无效则触发缺页异常,由操作系统处理;转换过程可通过TLB缓存和多级页表(如x86的分页机制)加速优化。核心思想是通过分页实现虚拟地址到物理地址的映射,隔离进程内存空间并支持高效内存管理。

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

在 TLB 和四级页表 的支持下,虚拟地址(VA)到物理地址(PA)的转换首先由 TLB 缓存 快速匹配,若未命中则通过 CR3 寄存器 定位顶级页表,依次遍历 PML4→PDPT→PD→PT 四级页表结构获取物理页框号,最终与页内偏移拼接形成物理地址,同时更新 TLB 以加速后续访问;若页表项无效则触发缺页异常,由操作系统处理,从而在保证内存隔离的同时实现高效地址转换。

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

在三级Cache(L1/L2/L3) 的支持下,CPU访问物理内存时首先查询高速缓存:若数据在 L1 Cache 命中则直接返回;若未命中则依次查询 L2 和 L3 Cache,仍缺失则通过内存控制器访问主存,并将数据按 缓存行粒度逐级回填,同时利用预取和替换算法优化命中率,减少直接访问主存的延迟,显著提升内存访问效率。

7.6 hello进程fork时的内存映射

当 hello进程调用 fork() 时,内核采用写时复制(Copy-On-Write, COW) 机制,父子进程共享相同的物理内存页,仅标记为只读;当任一进程尝试写入时,触发页错误,内核再分配新物理页并复制数据,确保内存修改隔离,从而高效实现进程创建,避免立即复制全部内存的开销。

7.7 hello进程execve时的内存映射

当 hello进程调用 execve() 加载新程序时,内核会释放原进程的地址空间,根据可执行文件格式(如ELF)重新构建代码段、数据段、堆、栈等内存映射区域,其中代码段和数据段通过文件映射直接关联到磁盘中的二进制文件(延迟加载),堆和栈则初始化为匿名映射;同时更新页表并重置进程的内存上下文,最终完成程序替换,而这一过程通常借助写时复制(COW)和按需分页(Demand Paging)机制优化性能,避免立即加载全部内容。

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

当进程访问的虚拟页未映射到物理内存时,触发缺页故障,CPU 将控制权转交给操作系统内核的缺页中断处理程序;内核根据缺页原因(如未分配、权限不足、写时复制或页面被换出)采取相应措施:若为合法访问,则分配物理页、加载磁盘数据(涉及换页或文件映射)并更新页表;若为非法访问(如越界或权限错误),则终止进程。

7.9动态存储分配管理

7.10本章小结

本章首先结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。随后分析了段式管理和页式管理的过程。阐述了TLB与四级页表支持下的VA到PA的变换,探究了三级Cache支持下的物理内存访问。随后结合hello进程,分析fork和execve运行时的内存映射。最后缺页故障与缺页中断处理的分析。


8hello的IO管理

8.1 Linux的IO设备管理方法

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

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

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

8.3 printf的实现分析

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

[转]printf 函数实现的深入剖析 - Pianistx - 博客园

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

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

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

8.4 getchar的实现分析

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

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

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

8.5本章小结

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

结论

1. 源代码编译(用户态)

·预处理(gcc -E hello.c):展开宏(如#include <stdio.h>)、删除注释,生成.i文本文件。

·编译(gcc -S hello.i):将C代码转换为汇编指令(.s文件),例如main中的printf调用被翻译为call puts@PLT(x86-64)。

·汇编(gcc -c hello.s):生成可重定位目标文件(.o),包含机器码(.text段)和符号表(如main、puts的未解析引用)。

2. 静态链接(ld hello.o -lc):合并多个.o文件,解析符号引用,生成完全链接的可执行文件hello(ELF格式)。

3. 动态链接与加载(内核态→用户态)

execve系统调用:加载hello时,内核读取ELF头部,动态链接器ld.so完成。

4. 进程内存初始化

·虚拟地址空间构建

代码段:.text映射到物理页(权限RX),标记为只读。

数据段:.data(已初始化变量)和.bss(未初始化变量)映射到匿名页。

堆/栈:通过brk和自动增长机制分配。

5. CPU执行与系统调用:CPU从_start开始执行,跳转至main。

·printf内部:用户态缓冲区填充字符串,通过syscall指令触发write(1, "Hello\n", 6)。

·陷入内核:切换到内核态,拷贝字符串到内核缓冲区,终端驱动(如TTY)处理输出。

6. 缺页异常处理(MMU协作)

·首次访问触发缺页,访问未加载的代码/数据页时,MMU触发缺页中断:

文件映射页(如.text):从磁盘读取页内容到物理内存,更新页表。

匿名页(如栈扩展):分配物理零页(Zero-Filled Page)。

·TLB更新:缓存新映射以加速后续访问。

7. 缓存层次访问(硬件加速)
CPU访问printf代码或字符串时,依次查询缓存层级,缺失时从主存加载缓存行(Cache Line)。

写回策略:printf的输出可能暂存于缓存,最终由内存控制器写回内存。

8. 进程终止:exit系统调用
释放资源:

内存:撤销所有mmap映射,清空页表。

文件描述符:关闭stdout等。

信号处理:向父进程(如Shell)发送SIGCHLD。

通过本次研究,我深刻体会到计算机系统的精妙之处在于其分层抽象与软硬件协同的设计哲学。从晶体管到操作系统,每一层都通过接口隐藏复杂性,同时提供高效的通用服务。在学习与实践过程中,我深刻体会到:性能、安全与可扩展性的平衡是系统设计的核心挑战。我的创新理念聚焦于以下方向:

·智能化内存管理:结合机器学习预测进程的内存访问模式,动态调整页替换算法(如改进的Clock算法),减少缺页率。

·异构计算透明化:设计统一抽象层,让程序无需修改即可自动适配CPU/GPU/TPU,通过运行时 profiling 动态分配计算任务。

·安全与性能共生的系统调用:提出“轻量级沙箱”机制,在用户态实现系统调用的快速验证(如eBPF扩展),减少内核切换开销。

·可持续系统设计:通过硬件感知的功耗调度算法(如动态调整CPU频率与缓存策略),优化能效比。


附件

文件名

作用

hello.c

原始C程序代码。

hello.i

预处理后得到的文本文件。

hello.s

目标平台汇编代码。

hello.o

包含机器码和符号表的可重定位文件。

hello

可执行文件。

hello.elf

用readelf读取出的hello.o的ELF格式信息。


参考文献

[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.

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

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

原文链接:https://blog.csdn.net/2503_91742236/article/details/148214065

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

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