【无标题】HIT-ICS2025计统大作业——程序人生 - 详解

计算机系统

大作业

题 目程序人生-Hello’s P2P

专 业计算机与电子通信

学 号 2023111374

班 级23L0501

学 生 马宇宸

指导教师 刘宏伟

计算机科学与技术学院

2024年5月

目 录

第1章 概述... - 1 -

1.1 Hello简介... - 1 -

1.2 环境与工具... - 2 -

1.3 中间结果... - 2 -

1.4 本章小结... - 3 -

第2章预处理... - 4 -

2.1 预处理的概念与作用... - 4 -

2.2在Ubuntu下预处理的命令... - 4 -

2.3 Hello的预处理结果解析... - 4 -

2.4本章小结... - 6 -

第3章编译... - 7 -

3.1编译的概念与作用... - 7 -

3.2在Ubuntu下编译的命令... - 7 -

3.3 Hello的编译结果解析... - 8 -

3.4本章小结... - 11 -

第4章 汇编... - 12 -

4.1汇编的概念与作用... - 12 -

4.2在Ubuntu下汇编的命令... - 12 -

4.3可重定位目标elf格式... - 12 -

4.4 Hello的结果解析... - 17 -

4.5本章小结... - 19 -

第5章 链接... - 20 -

5.1链接的概念与作用... - 20 -

5.2在Ubuntu下链接的命令... - 20 -

5.3可执行目标材料hello的格式... - 21 -

5.4 Hello的虚拟地址空间... - 23 -

5.5链接的重定位过程分析... - 24 -

5.6 Hello的执行流程... - 25 -

5.7 Hello的动态链接分析... - 28 -

5.8本章小结... - 29 -

第6章 Hello进程管理... - 30 -

6.1进程的概念与作用... - 30 -

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

6.3 Hello的fork进程创建过程... - 30 -

6.4 Hello的execve过程... - 31 -

6.5 Hello的进程执行... - 31 -

6.6 Hello的异常与信号处理... - 31 -

6.7本章小结... - 34 -

第7章Hello的存储管理... - 35 -

7.1 Hello的存储器地址空间... - 35 -

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

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

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

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

7.6 Hello进程fork时的内存映射... - 36 -

7.7 Hello进程execve时的内存映射... - 36 -

7.8 缺页故障与缺页中断处理... - 37 -

7.9动态存储分配管理... - 37 -

7.10本章小结... - 38 -

第8章Hello的IO管理... - 39 -

8.1 Linux的IO设备管理方法... - 39 -

8.2简述UnixIO接口及其函数... - 39 -

8.3 Printf的达成分析... - 40 -

8.4 Getchar的实现分析... - 40 -

8.5本章小结... - 41 -

结论... - 42 -

附件... - 43 -

参考文献... - 44 -

第1章 概述

1.1Hello简介

      1. P2P:从程序到进程的生命周期
        1. 代码与编译阶段

源码:用户编写hello.c,具备参数检查、循环打印、延时及输入等待逻辑。

预处理:展开头文件,生成hello.i。

编译:转换为hello.s,printf调用对应callprintf@PLT。

汇编与链接:生成目标文件hello.o,动态链接libc.so,最终生成可执行文件hello。

        1. 进程创建与加载

Shell调用:用户输入./hello学号姓名手机号秒数,触发fork()创建子进程。

execve加载:子进程调用execve(),替换为hello的代码段(.text)、信息段(.data、.bss)。

内存映射:OS经过mmap分配虚拟地址空间,映射代码、堆栈、共享库(如libc.so)。

        1. 进程执行与硬件交互

CPU调度:OS分配时间片,CPU流水线执行指令。

存储管理:MMU借助4级页表将VA转换为PA,TLB缓存加速地址转换。

I/O与信号:printf通过内核写入显存,显卡渲染到屏幕。sleep(秒数)让出CPU,getchar()阻塞等待输入。Ctrl-C发送SIGINT终止进程,Ctrl-Z挂起进程(可通过fg恢复)。

      1. O2O:资源从零分配至完全回收
        1. 资源初始化(FromZero)

OS分配:进程获得PID、虚拟内存空间、记录描述符(stdin/stdout/stderr)。

上下文构建:栈初始化(参数argv、环境变量),PC指向main入口。

        1. 运行时资源管理

动态内存:若使用malloc,堆通过brk扩展。

页表与Cache:TLB和三级Cache加速内存访问,缺页中断加载物理页。

信号处理:内核拦截SIGINT/SIGTSTP,触发进程终止或挂起。

        1. 资源回收(ToZero)

正常退出:return0调用exit(),释放内存、关闭文件描述符。

强制终止:Ctrl-C触发内核回收资源,无需父进程等待。

僵尸处理:Shell通过wait()回收子进程状态,释放PID,清除进程表项。

归零状态:所有资源(内存、PID、记录句柄)归还系统,进程痕迹完全消失。

1.2 环境与工具

Windows11 64位;Vmware Workstation Pro(Vmware17);Ubantu18.04

1.3 中间结果

      1. 初始代码文件

hello.c

文件类型:C语言源代码文件(用户编写的原始程序)。

作用:c代码包含程序逻辑,是编译过程的输入起点,后续所有中间文件均由其衍生。代码中的系统调用,例如printf、sleep依赖头文件#include的展开。

      1. 编译阶段生成的中间文件
        1. hello.i

生成方式:预处理阶段(gcc -E hello.c -o hello.i)

作用:

展开所有#include头文件,插入原始代码。

处理宏定义(如#define)和条件编译指令(如#ifdef)。

删除注释,生成纯C代码,供后续编译阶段使用。

        1. hello.s

生成方式:编译阶段(gcc -S hello.i -o hello.s)

作用:

将预处理后的C代码转换为汇编语言代码(如x86-64指令)。

包含符号引用(如call printf@PLT),但尚未绑定实际地址(需链接阶段解决)。

        1. hello.o

生成方式:汇编阶段(gcc -c hello.s -o hello.o)

作用:

将汇编代码转换为可重定位目标文件(RelocatableObjectFile)。

包含机器指令(.text段)、全局变量(.data/.bss段)和重定位信息(如printf的符号表条目)。

未完成最终地址绑定,需链接器处理动态库依赖。

        1. hello

生成方式:链接阶段(gcc hello.o -o hello)

作用:

将hello.o与C标准库(libc.so)动态链接,生成可执行文件。

固定代码段地址(因-no-pie选项),分配虚拟内存布局(代码段、数据段、堆栈等)。

1.4 本章小结

本章概述了“Hello’s P2P”程序从源码到进程的完整生命周期(P2P)及资源从分配到回收的全过程(O2O)。通过分析预处理、编译、链接等阶段生成的中间文件,明确了各阶段的核心任务与依赖关系。实验环境基于Ubuntu系统,使用GCC设备链完成代码转换,并凭借fork与execve机制完成进程的动态加载。程序运行过程中,操作系统经过虚拟内存管理、TLB与Cache加速、信号处理等机制保障了执行效率与安全性。本章为后续章节的深入分析奠定了基础,揭示了应用在计算机系统中从抽象逻辑到物理硬件的多层次映射关系。

第2章预处理

2.1预处理的概念与作用

      1. 概念

预处理是C软件编译的第一个阶段,由预处理器执行,主要任务是对源代码进行文本替换和条件处理,生成经过处理的中间代码(.i文件)。

      1. 作用

头文件展开:将#include<header.h>指令替换为头文件的实际内容。

例如,#include<stdio.h>会被替换为stdio.h中的函数声明和宏定义。

宏替换:展开#define定义的宏。例如,#defineMAX100在代码中的MAX会被替换为100。

条件编译:根据#ifdef、#if等指令选择性地含有或排除代码块。

删除注释:移除所有单行(//)和多行注释(/*...*/)。

添加行号标记:生成#line指令,便于编译器定位错误位置。

2.2在Ubuntu下预处理的命令

运用gcc-Ehello.c-ohello.i命令,指示编译器仅执行预处理阶段,并指定输出文件名为hello.i。再使用ls命令进入目录,即可观察到生成的hello.i文件。

2.3 Hello的预处理结果解析

hello.i文件内容分析

      1. 头文件展开

#include<stdio.h>被替换为stdio.h中的全部内容,其中本例涉及到的为涵盖printf函数的声明:externintprintf(constchar*__restrict__format,...)。

      1. 注释删除

原始代码中的注释(如//大作业的hello.c程序)被完全删除。

      1. 行号标记

插入#line指令,标识原始代码位置。

PS:本代码虽未涉及宏定义,但是宏处理也是预处理中极为重要的一个操作,对#define定义的宏进行展开,直到文本文件中没有宏定义为止。

hello.i档案截图如下:

2.4本章小结

预处理是C程序编译的起点,其核心任务是通过文本替换和条件处理,生成纯净的中间代码(.i资料)。

      1. 重要性:

为后续编译阶段(如语法分析、代码生成)供应无歧义的输入。

凭借宏和条件编译增强代码的可维护性和跨平台性。

      1. 在Hello工具中的体现:

stdio.h等头文件的展开为printf和exit供应了必要声明。

删除注释和添加行号标记优化了代码结构,便于编译器处理。

预处理结果(hello.i)直接影响了后续编译阶段的行为,是脚本从源代码到可执行文件的关键过渡。

第3章编译

3.1编译的概念与作用

      1. 概念

编译是将预处理后的中间文件(.i)转换为汇编语言记录(.s)的过程,由编译器(如gcc的cc1组件)完成。其核心任务是对(C代码进行语法分析、语义检查和优化,最终生成与目标平台相关的汇编指令。

      1. 作用

语法分析:检查代码是否符合C语言语法规则(如括号匹配、语句结束符)。

否一致等逻辑正确性。就是语义检查:验证变量类型匹配、函数声明与调用

代码优化:对冗余计算、循环结构等进行低级优化(如常量折叠、死代码删除)。

生成汇编代码:将C语言抽象逻辑映射为机器可理解的汇编指令(如mov、call)。

3.2在Ubuntu下编译的命令

使用gcc-Shello.i-ohello.s命令,指示编译器执行到汇编阶段前停止,生成汇编文档,并指定输出文件名为hello.s。

Hello.s文件的全部内容:

3.3 Hello的编译结果解析

此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类管理的。应分3.3.1~3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与运行,都应解析。

3.3.1信息存储与初始化

        1. 局部变量(inti)

存储位置:局部变量i分配在栈帧中(地址-4(%rbp))。

初始化:循环开始前初始化为0,对应汇编指令:

        1. 字符串常量

存储段:"Hello%s%s%s\n"存储在只读内容段(.rodata),标签为.LC0:

        1. 函数参数(argv数组)

指针操控:argv作为指针数组,通过基址寄存器%rsi访问,被保存到栈帧的-32(%rbp)位置。

以argv[1]的访问过程为例,argv[1]的地址通过基地址-32(%rbp)加上偏移量8计算得出。

3.3.2类型转换与sizeof

        1. 显式类型转换(atoi(argv[4]))

逻辑:将字符串参数转换为整型,调用atoi函数:

        1. 隐式类型提升

sleep参数传递:atoi返回的int直接作为sleep参数(无需额外转换)。

        1. sizeof处理

指针大小:argv为char**类型,64位系统中指针大小为8字节,通过movq指令执行,经过偏移量体现指针大小。例如argv[1]的偏移量是8字节。

3.3.3算术与复合操控

        1. 自增操作(i++)

通过addl指令完成,

        1. 复合赋值(sleep(atoi(argv[4])))

逻辑:atoi返回值直接传递给sleep,无显式中间变量。

3.3.4逻辑与位操作

        1. 条件判断(if(argc!=5))

比较argc(存储在%edi)是否等于5。

        1. 逻辑非(!)与短路求值

循环条件(i<10):隐式转换为i<=9,依据cmpl和jle达成:

3.3.5关系操作与控制转移

        1. for循环结构

初始化:i=0,存储在栈中。

条件检查:

迭代更新:

        1. if-else分支

错误处理分支:若argc!=5,调用printf和exit(1):

(正确处理分支在3.3.4.1中体现)

3.3.6数组与指针操作

        1. 数组访问(argv[1])

地址计算:argv基址为%rsi,偏移量8(8*1)获取argv[1]

        1. 指针解引用:

printf参数传递:argv[1]作为指针,直接通过寄存器传递

3.3.7函数操作

        1. 参数传递规则

寄存器传参:前6个参数依次使用%rdi,%rsi,%rdx,%rcx,%r8,%r9。

        1. 函数调用与返回

printf调用:通过PLT实现动态链接,并实现main函数返回0的操作。

        1. 栈帧管理

栈指针执行:%rbp作为基址指针,局部变量通过负偏移访问。

3.4本章小结

编译阶段是C程序转换为机器可执行代码的关键步骤。

核心任务:将高级C代码逻辑映射为平台相关的汇编指令,同时进行语法检查和优化。

数据类型与操作映射:

基本类型(如int)借助寄存器或栈空间存储。

控制结构(如for、if)转换为条件跳转指令(jle、je)。

函数调用遵循ABI规范(如寄存器传参、栈帧管理)。

优化体现:

循环计数器i直接使用寄存器运行,避免内存频繁访问。

字符串常量存储在只读段,提升执行效率。

编译生成的汇编代码(.s文件)为后续汇编阶段给予了明确的机器指令基础,是代码从抽象逻辑到硬件执行的重要桥梁。

第4章 汇编

4.1汇编的概念与作用

      1. 概念

汇编是将汇编语言代码(.s文件)转换为机器语言二进制目标文件(.o文件)的过程,由汇编器(如as或gcc的汇编组件)完成。

      1. 作用:

指令编码:汇编指令(如mov,call)转换为机器指令(二进制操作码)。

符号解析:标记未解析的符号(如printf),生成重定位条目供链接器处理。

生成可重定位目标文件:包含代码段(.text)、数据段(.data,.rodata)和重定位信息。

4.2在Ubuntu下汇编的命令

使用gcc-chello.s-ohello.o命令,指示编译器执行到汇编阶段前停止,生成汇编文件,并指定输出文件名为hello.o。

4.3可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

      1. ELF头部信息

在头部信息中,我们许可看出有13个节头表项,给出了ELF文档的标识、版本、64位架构、小端存储、目前的入口地址(未链接故没有)等重要信息。

      1. 查看节头表(SectionHeaders)

运用readelf-Shello.o命令,对节头表进行查看。

关键节信息:

.text:存储机器指令(如main函数的代码)。

.rodata:只读数据(如字符串常量"Hello%s%s%s\n")。

.data/.bss:全局变量(本例中无显式定义)。

.rela.text:代码段的重定位条目。

其他信息:

.bss:未初始化的全局/静态变量

.rela.text:代码段的重定位条目(如printf、exit的函数地址需链接时填充)。

.comment:编译器注释信息(如GCC版本和编译选项)。

.note.GNU-stack:标记栈执行权限(通常为空,表示禁止栈执行)。

.symtab:符号表,记录代码中定义的符号(如main)和引用的外部符号(如printf)。

.strtab:字符串表,存储符号名称(如main、printf的字符串)。

      1. 查看重定位条目

使用readelf-rhello.o命令,查看重定位条目。

输出如下:

发现.rela.text段一共有8个条目,.rela.eh_frame段有1个重定位条目,下面对其分别解析:

        1. rela.text段

记录了所有函数调用和全局数据访问的重定位需求,是链接器生成可执行文件的关键依据。

【条目1

Offset:0x18

Type:R_X86_64_PC32

Symbol:.rodata

Addend:-4

解析:该条目对应代码中访问只读数据段(.rodata)中的字符串常量的指令。例如,printf("Hello%s%s%s\n")中格式字符串存储在.rodata中,此处需计算其相对地址。

R_X86_64_PC32表示采用32位相对地址偏移,Addend=-4用于修正指令中操作数的偏移量。

【条目2

Offset:0x1d

Type:R_X86_64_PLT32

Symbol:puts

Addend:-4

解析:对应代码中调用puts("用法:Hello...")的指令。R_X86_64_PLT32表示通过PLT(过程链接表)调用动态库函数,支持延迟绑定。

Addend=-4修正call指令的偏移量(PLT表项地址)。

【条目3

Offset:0x27

Type:R_X86_64_PLT32

Symbol:exit

Addend:-4

解析:对应代码中调用exit(1)的指令。同条目2,应用PLT机制调用exit。

【条目4

Offset:0x5b

Type:R_X86_64_PC32

Symbol:.rodata

Addend:+0x2c

解析:对应另一处访问.rodata的指令。Addend=+0x2c表示该字符串在.rodata段中的偏移量为0x2c(如第二个字符串常量)。

【条目5

Offset:0x65

Type:R_X86_64_PLT32

Symbol:printf

Addend:-4

解析:对应代码中printf("Hello%s%s%s\n",...)的调用。使用PLT机制调用printf,Addend修正call指令的偏移量。

【条目6

Offset:0x78

Type:R_X86_64_PLT32

Symbol:atoi

Addend:-4

解析:对应代码中atoi(argv[4])的调用。利用PLT调用atoi,将字符串参数转换为整型。

【条目7

Offset:0x7f

Type:R_X86_64_PLT32

Symbol:sleep

Addend:-4

解析:对应代码中sleep(atoi(...))的调用。使用PLT机制调用sleep。

【条目8

Offset:0x8e

Type:R_X86_64_PLT32

Symbol:getchar

Addend:-4

解析:对应代码中getchar()的调用。通过PLT调用getchar,等待用户输入。这些函数在符号表均可看到,

        1. rela.eh_frame段

确保异常处理机制能正确关联代码段。

Offset:0x20

Type:R_X86_64_PC32

Symbol:.text

Addend:0

解析:.eh_frame用于异常处理(ExceptionHandling),记录函数调用栈的布局信息。该条目确保异常处理框架能正确引用.text段的起始地址。

R_X86_64_PC32表示相对地址偏移,Addend=0表示直接指向.text的起始位置。

        1. 对于以上涉及到的关键重定位进行类型说明

R_X86_64_PC32

用途:用于计算32位相对地址偏移(目标地址相对于下一条指令的偏移)。

场景:访问.rodata中的字符串常量或局部静态数据。

R_X86_64_PLT32

用途:通过PLT(过程链接表)调用动态库函数,协助延迟绑定。

场景:调用外部函数(如printf、exit)

4.4 Hello的结果解析

      1. 函数调用(printf)的映射

callq的机器码,表示调用函数。就是操作码:e8

操作数:00000000是占位符,实际地址需链接时通过重定位条目R_X86_64_PLT32填充。

重定位条目:printf的地址在链接时从动态库libc.so解析,修正占位符为实际偏移量。

      1. 分支转移(jle)的映射

操作码:7e是jle的机器码,表示“小于或等于时跳转”。

操作数:e8是相对偏移量(补码表示),计算方式为目标地址-下一条指令地址=0xe8-0x7c=-20(实际跳转至0x64)。

      1. 数据访问(.rodata字符串)的映射

操作码:488d3d对应lea指令,用于计算地址。

操作数:00000000为占位符,实际偏移量由重定位条目R_X86_64_PC32填充。

      1. 总结

机器语言与汇编语言的核心差异体现在操作数处理与地址解析机制。汇编代码中的符号(如函数名.L3、printf@PLT或数据标签.LC0)在机器码中转为相对偏移占位符,因符号地址在汇编阶段未确定,需链接时经过重定位条目(如R_X86_64_PLT32)动态修正。函数调用(如call)和跳转指令的操作数在机器码中为二进制占位符,链接器结合PLT表项地址填充;立即数(如$9)直接编码为常量,而内存访问(如-4(%rbp))通过基址寄存器与偏移量(如fc)组合表示。这种分层机制确保了代码的可重定位性与硬件执行的高效性。

4.5本章小结

汇编阶段是程序从汇编代码(.s)到机器语言目标文件(.o)的关键转换过程。

核心任务:

将汇编指令编码为机器码,生成可重定位目标文件。

标记未解析符号,生成重定位条目供链接器使用。

ELF格式结构:

.text、.data、.rodata等段存储代码和数据。

.rela.text记录代码段中需重定位的符号地址。

机器语言特性:

操作码与操作数的二进制表示需符合CPU指令集规范。

分支和函数调用的地址在汇编阶段为占位符,链接时修正。

与汇编代码的差异:

符号引用(如printf)在机器语言中表现为重定位条目,而非具体地址。

相对跳转偏移量需在链接时根据目标地址计算。

汇编阶段为链接器供应了可重定位的二进制基础,是程序从源码到可执行文件的关键过渡。

第5章 链接

5.1链接的概念与作用

      1. 概念

链接是将多个目标文件(.o)和库文件(如libc.so)合并为单一可执行文件(hello)的过程,由链接器(如ld)完成。

      1. 作用

符号解析:解决未定义符号(如printf)的引用。

地址分配:为代码段、信息段分配虚拟内存地址。

重定位:修正目标资料中的符号地址占位符,生成可执行的机器码。

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\

>-lc\

>/usr/lib/x86_64-linux-gnu/crtn.o\

>-ohello命令,指定动态链接器路径,链接C标准库。

再凭借ls指令打开目录,发现生成了可执行文件hello,说明链接已成功完成。

也可从图标看出,生成了hello文件。

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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

      1. 借助readelf-ahello指令,查看ELF头部信息

可以看出,这次的头部信息跟刚刚有很大不同。显然,这次的类型为EXEC,表示已经链接完成,可直接运行。也给出了入口地址为0x400550,指向_start或main函数地址。

      1. ELF节头表信息

该节头表完整描述了可执行文件hello的内存布局与功能模块:

代码与数据:.text、.rodata、.data分别存储指令、常量和变量。

动态链接:.interp、.dynamic、.got.plt支持运行时加载共享库。

符号与调试:.symtab、.eh_frame提供符号和异常处理信息(通常发布时被剥离)。

初始化与终止:.init和.fini管理全局对象的构造与析构。

性能优化:.gnu.hash和.plt加速符号解析与函数调用。

5.4 Hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

使用edb--runhello指令启动edb,在datadump中根据ELF节头表信息得到每个功能模块的偏移量,从而得出该模块的虚拟地址。初始地址为004000000

      1. Text段

从5.3可知,text段的偏移为00000550,得出其虚拟地址为00400550,在edb找出其对应的虚拟地址便可找到其数据信息。

      1. Rodata段

同理,计算出rodata段的虚拟地址为004006a0,找到其数据信息。

      1. 堆栈分布

栈地址从00007fffbd0f9000开始,堆从00600000动态增长。

5.5链接的重定位过程分析

      1. 函数调用解析(以printf函数为例)

在hello.o中,因为它尚未进行链接,故callprintf@PLT表明printf为未解析符号,依赖重定位条目填充地址;但在已链接的hello中,地址0x400500指向.plt中的printf@plt条目,PLT经过GOT实现延迟绑定。

      1. 全局变量与信息访问

hello.o在访问全局数据时,需使用相对地址占位符;在hello中,地址已修正为实际虚拟地址,

0x4006d8是.rodata段的实际地址,存储格式字符串"Hello%s%s%s\n"。

      1. 入口点与初始化代码

hello.o无入口点,无初始化代码;hello的入口点为_start(0x400550),包含完整的初始化代码(.init)和终止代码(.fini)。

5.6 Hello的执行流程

将“使用gdb/edb执行hello,说明从加载hello到_start,到callmain,以及程序终止的所有过程”转化为gdb逻辑,就需要在入口、main函数处设置断点。

gdbhello

(gdb)break_start

(gdb)breakmain

(gdb)run2023111374myc185036670013

运用这四个语句,便可调用gdb对程序运行状态进行监控。

      1. 程序在入口点_start处

在这段代码中,程序实现了初始化栈帧、对齐堆栈、准备参数并调用__libc_start_main,最终跳转到main。

核心调用链与地址:

指令地址

调用目标

0x400550

_start

0x400574

__libc_start_main@got.plt

0x400582

main

      1. 进入main函数

输入continue,让程序运行到第二个断点。

  1. 在main函数内部中,有许多函数的内部调用,如指令地址0x40059e,调用目标为puts@plt(0x4004f0)、0x4005a8,调用目标为exit@plt(0x400530)......在此列出一个调用表格:

指令地址

调用目标

0x40059e

puts@plt(0x4004f0)

0x4005a8

exit@plt(0x400530)

0x4005e6

printf@plt(0x400500)

0x4005f9

atoi@plt(0x400520)

0x400600

sleep@plt(0x400540)

0x40060f

getchar@plt(0x400510)

  1. 间接跳转与动态链接。

地址

目标

说明

0x4004f0

puts@got.plt(0x601018)

通过PLT跳转到GOT表项,首次调用触发动态链接器解析puts地址。

0x400530

exit@got.plt(0x601038)

调用exit时跳转到GOT表项,动态解析exit地址。

0x400500

printf@got.plt(0x601020)

printf的PLT条目,首次调用时解析libc中的实际函数地址。

0x400540

sleep@got.plt(0x601040)

sleep的PLT条目,延迟绑定到libc的实现。

      1. 终止阶段

地址

函数/指令

说明

0x4005a8

callq0x400530<exit@plt>

若参数错误,直接调用exit终止进程。

0x400619

leaveq

main函数返回,控制权交还__libc_start_main

0x400694

_fini

全局析构函数,释放全局对象和资源。

0x400530

exit@plt

最终调用exit,触发系统调用exit_group终止进程并回收资源。

5.7 Hello的动态链接分析

起初,定位关键节信息,依据readelf查找.got.plt和printf的重定位信息:

可知,printf的GOT条目位于0x601000。

程序停在main入口时,利用edb查看动态链接前的段内容:

在动态链接后,got.plt段内容如下,0x601000的值变为printf的实际地址:

在动态链接过程中,.got.plt的初始条目指向PLT中预设的符号解析逻辑(如push重定位索引并跳转至动态链接器),而PLT通过jmp*GOT[n]设计实现延迟绑定机制。当程序首次调用共享库函数(如printf)时,控制流通过PLT跳转至.got.plt中未解析的地址,触发动态链接器(_dl_runtime_resolve)根据重定位表(.rela.plt)解析真实函数地址,并将结果回填到.got.plt。此后,.got.plt条目直接指向目标函数(如0x7ffff7e3e420),后续调用无需重复解析。这种机制既通过地址无关代码(PIC)支持动态库灵活加载,又通过首次调用按需解析显著降低启动开销,平衡了灵活性与性能。

5.8本章小结

链接是程序从目标文件到可执行文件的关键步骤,核心任务包括符号解析、地址分配与重定位。

静态链接:合并目标文件和静态库,生成完全独立的可执行文件。

动态链接:运行时通过PLT/GOT机制绑定动态库函数,减少内存占用。

ELF结构:段表、符号表、重定位条目等元数据承受链接器高效工作。

虚拟地址空间:链接器为各段分配固定虚拟地址,操作系统通过页表映射到物理内存。

链接过程体现了计算机系统分层协作的核心思想,是程序最终运行的基石。

第6章 Hello进程管理

6.1进程的概念与作用

      1. 概念

进程是程序的执行实例,拥有独立的地址空间、代码段、数据段和资源(如文件描述符、CPU时间片)。

      1. 作用

资源分配:操作系统通过进程隔离资源(内存、CPU),避免程序间冲突。

并发执行:多进程并发运行,提升框架效率。

状态管理:进程通过就绪、运行、阻塞等状态实现调度。

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

      1. 作用

命令解析:将用户输入的命令解析为可执行程序或内置命令。

进程管理:经过fork和execve创建新进程,支持后台任务(&)、管道(|)和重定向(>)。

环境维护:管理环境变量(如PATH)和进程组。

      1. 处理流程:
  1. 读取输入:从终端或脚本读取命令。
  2. 解析命令:分割参数,处理特殊符号(如*通配符)。
  3. 执行命令:

内置命令:直接执行(如cd、export)。

外部程序:调用fork创建子进程,子进程调用execve加载程序(如./hello)。

等待做完:若未指定后台运行,父进程(Shell)调用wait等待子进程结束。

6.3 Hello的fork进程创建过程

当用户在Shell中输入./hello命令后,Shell通过fork系统调用创建子进程。fork会复制当前进程(父进程)的完整上下文,包括代码段、资料段、堆栈和打开的文件描述符,生成一个几乎完全相同的子进程。此时,父子进程共享同一份物理内存页,但通过写时复制(Copy-on-Write,COW)机制优化性能——仅当任一进程尝试修改内存时,内核才会为修改的页创建独立副本。fork的返回值区分父子进程:父进程接收到子进程的PID,继续运行Shell逻辑;子进程则返回0,准备通过execve加载新程序hello。子进程继承父进程的环境变量、信号处理方式及文件描述符(如标准输入/输出),但通过进程调度器独立运行,与父进程竞争CPU时间片,实现并发执行。

6.4 Hello的execve过程

子进程在调用execve("./hello",argv,environ)后,内核先验证hello的ELF文件格式及执行权限,随后彻底替换子进程的地址空间。原Shell子进程的代码段被替换为hello的指令,信息段、堆栈和堆重新初始化为hello程序定义的内容,同时保留继承的文档描述符(除非显式关闭)。内核将命令行参数argv和环境变量environ压入新堆栈,并将程序计数器(PC)指向hello的入口点_start,最终调用main函数。execve成功后,原Shell子进程的代码完全被hello替代,进程身份转变为hello,开始执行用户定义的逻辑(如循环打印)。这一过程达成了进程的“脱胎换骨”,仅保留必要的资源(如PID),确保新代码独立运行。

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

  1. 进程调度与上下文切换:

时间片分配:操作系统为hello分配CPU时间片(如10ms),基于CFS(完全公平调度器)算法。

上下文信息:

寄存器状态:保存工具计数器(PC)、栈指针(SP)、通用寄存器。

内存映射:页表记录虚拟地址到物理地址的映射。

  1. 用户态与内核态转换:

系统调用:如printf触发write,进程从用户态陷入内核态。

中断处理:时钟中断强制进程让出CPU,触发调度器切换进程。

执行流程:hello在用户态执行循环打印,调用sleep时主动让出CPU,进入阻塞状态。

6.6 Hello的异常与信号处理

在Linux系统中,程序运行过程中可能触发多种异常,内核会将异常转化为信号发送给进程。以下是hello软件可能遇到的异常、信号及其处理方式:

      1. 常见异常与对应信号

异常类型

触发场景

信号

默认处理

用户中断

按下Ctrl-C(键盘中断)

SIGINT

终止进程

进程挂起

按下Ctrl-Z(键盘暂停)

SIGTSTP

挂起进程(进入后台)

非法内存访问

访问无效内存地址(段错误)

SIGSEGV

终止进程并生成核心转储

算术错误

除零操作

SIGFPE

终止进程

子进程终止

子进程退出未处理

SIGCHLD

忽略

      1. 键盘操作与信号处理
  1. 启动程序

我尝试不停的乱按、回车,字符。它们暂时存在终端输入缓冲区,代码不主动读取则不会处理,并不会影响./hello的执行。

  1. 按下Ctrl-Z

触发信号:SIGTSTP(暂停进程)。

进程被挂起到后台,终端表明作业号。

  1. Ps

查看进程状态,STAT字段为T表示暂停。

  1. Fg

将挂起的进程恢复到前台继续运行。

  1. Jobs

显示当前终端的后台作业。

  1. Pstree

查看进程树。

  1. kill

发送信号杀死对应进程。

  1. 按下Ctrl-C

触发信号:SIGINT,使进程立即终止。

6.7本章小结

进程管理是操作系统核心能力,hello的生命周期由以下步骤构成。

创建:Shell依据fork复制自身,子进程execve加载hello。

执行:进程在用户态执行逻辑,通过时间片轮转和体系调用与内核交互。

信号处理:外部中断(如Ctrl-C)触发信号,进程按默认或自定义处理程序响应。

终止:正常结束(exit)或强制终止(kill)。

操作系统通过进程调度、内存管理和信号机制,实现多任务的高效并发与资源隔离。

7章Hello的存储管理

7.1 Hello的存储器地址空间

在计算机系统中,地址空间分为多个层次:

  1. 逻辑地址:由程序直接生成的地址,通常表示为“段选择子:段内偏移”(如cs:0x400550)。在平坦内存模式下,段基址为0,逻辑地址等于段内偏移。
  2. 线性地址:段式管理后的地址,由逻辑地址利用段描述符转换得到。现代操作系统通常禁用分段,逻辑地址直接作为线性地址。
  3. 虚拟地址:进程视角的地址空间,通过页表映射到物理地址。例如,hello的代码段虚拟地址为0x400550,内容段为0x601048。
  4. 物理地址:实际内存芯片中的地址,由MMU通过页表转换得到。例如,0x400550可能映射到物理地址0x1af3d000。

示例:当main函数访问argv[1]时,虚拟地址0x7fffffffe008(栈中参数地址)通过页表转换为物理地址0x3e8d0008,完毕内存访问。

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

Intel处理器的段式管理通过段选择子和段描述符实现:

  1. 段选择子:16位寄存器(如CS、DS)指向全局描述符表(GDT)或局部描述符表(LDT)中的条目。
  2. 段描述符:包含段基址、段限长和权限(如可读、可执行)。
  3. 转换公式:

线性地址=段基址+逻辑偏移地址

线性地址=段基址+逻辑偏移地址

在hello中:操作系统启用平坦模式(FlatMode),段基址为0,逻辑地址直接作为线性地址(如0x400550)。

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

现代环境采用页式管理将线性地址转换为物理地址:

  1. 页表结构:四级页表(PML4→PDPT→PD→PT),每级页表项(PTE)包含下一级表或物理页的地址。
  2. 地址划分:64位虚拟地址分为四级索引(各9位)和页内偏移(12位)。
  3. 转换过程:

CPU借助CR3寄存器找到PML4表基址。

依次索引PDPT、PD、PT,最终获取物理页号(PFN)。

物理地址=PFN<<12+页内偏移。

示例:hello的代码地址0x400550转换时,CR3指向进程页表基址,经过四级索引找到物理页0x1af3d,最终物理地址为0x1af3d550。

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

TLB(TranslationLookasideBuffer):缓存近期使用的虚拟页到物理页的映射,加速地址转换。

  1. 四级页表查询流程:

检查TLB是否命中,若命中直接获取PFN。

未命中时遍历四级页表,更新TLB。

  1. 优化效果:TLB命中率越高,地址转换延迟越低。例如,hello循环执行时,代码页的TLB命中率接近100%。

7.5 三级Cache帮助下的物理内存访问

CPU利用三级Cache减少物理内存访问延迟:

  1. Cache层次:

L1Cache:分指令和数据Cache(如32KB),访问延迟1~4周期。

L2Cache:统一缓存(如256KB),延迟约10周期。

L3Cache:共享缓存(如8MB),延迟约40周期。

  1. 缓存行:64字节,通过物理地址索引。
  2. 访问流程:

L1命中:直接读取数据。

L1未命中→L2→L3→内存,耗时递增。

示例:hello首次访问字符串"Hello"时,触发L3Cache未命中,从内存加载数据;后续访问命中L1Cache。

7.6 Hello进程fork时的内存映射

当调用fork创建子进程时,操作系统采用写时复制(Copy-on-Write,COW)机制优化内存启用。父进程和子进程共享物理内存页,包括代码段(.text)和只读数据段(.rodata),这些页被标记为只读以保护共享内容。对于可写内存(如堆、栈和用户素材段),初始时仍共享物理页,但任一进程尝试修改这些页时,内核会触发缺页中断,为修改的页创建独立的物理副本,并更新子进程的页表以实现隔离。例如,hello进程的argv数组指针在fork后与父进程共享同一物理页,若子进程修改argv内容,内核会复制新页并分离映射。此外,子进程会继承父进程的档案描述符表,但档案偏移量独立管理,确保并发操控的一致性。这种机制显著减少了进程创建的开销,同时保证了内存安全和效率。

7.7 Hello进程execve时的内存映射

execve系统调用彻底替换进程的地址空间以加载新程序。内核首先释放原进程的代码段、数据段和堆栈,然后从hello的ELF记录中重新构建内存布局:

  1. 代码段映射:将hello的.text段加载到固定虚拟地址(如0x400000),并标记为可执行。
  2. 材料段初始化:已初始化的全局变量(.data段)和未初始化数据(.bss段)映射到0x600000附近,分别设置读写权限。
  3. 堆栈设置:初始化堆空间(通过brk指针动态扩展)和用户栈,将命令行参数argv和环境变量environ压入栈顶。
  4. 动态链接:加载共享库(如libc.so),通过PLT(过程链接表)和GOT(全局偏移表)完成函数地址的延迟绑定。例如,execve后原Shell子进程的代码被hello完全替代,栈中填充参数argv和环境变量,PLT在首次调用printf时通过动态链接器解析实际地址。此过程实现了进程的“脱胎换骨”,仅保留PID和部分资源,确保新脚本独立运行。

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

  1. 缺页中断是CPU访问未映射或权限不足的虚拟地址时触发的异常,核心场景包括:

访问未分配页:如malloc分配的堆内存首次被读写。

写只读页:尝试修改标记为只读的页(如COW共享页)。

页未加载:虚拟地址有效,但对应物理页未从磁盘加载(如内存交换场景)。

  1. 处理流程:

中断处理:CPU保存上下文后陷入内核,内核检查缺页原因。

物理页分配:若为合法访问(如堆内存首次使用),内核分配物理页并清零;若为COW触发的写操作,则复制新页并更新页表。

页表更新:将物理页号(PFN)写入页表项,标记为可读写。

恢复执行:CPU重新执行触发缺页的指令。

      1. 例如,hello调用malloc分配内存后,首次访问该内存会触发缺页中断,内核分配物理页并初始化为零,确保工具安全访问。这一机制实现了按需分配内存,避免不必要的物理内存占用,同时支持虚拟内存的动态扩展。

7.9动态存储分配管理

      1. 动态内存管理通过malloc和free达成:
  1. 分配方法:

隐式空闲链表:通过头部标记块状态(已分配/空闲)。

显式空闲链表:维护空闲块链表,加速搜索。

  1. 分配策略:

首次适应:选择第一个足够大的空闲块。

最佳适应:选择最小的足够块,减少碎片。

      1. printf中的malloc

格式化字符串缓冲区可能动态分配,使用brk扩展堆或mmap映射大块内存。

示例:printf内部调用malloc(1024)分配缓冲区,首次适应策略找到合适空闲块。

7.10本章小结

本章深入分析了hello脚本的存储管理机制:

地址空间:逻辑地址经段页式管理转换为物理地址,TLB和Cache加速访问。

进程内存:fork通过COW优化内存复制,execve彻底重建地址空间。

动态分配:malloc基于堆管理策略分配内存,缺页中断保障按需加载。

性能优化:多级页表、TLB和Cache层级显著降低内存访问延迟。

存储管理是操作系统核心功能,保障进程隔离性、安全性及资源高效利用。

8章Hello的IO管理

8.1 Linux的IO设备管理方法

Linux体系遵循“一切皆文件”的设计哲学,将硬件设备抽象为文件,凭借统一的UnixIO接口管理所有输入输出执行。

      1. 设备模型化

设备资料:硬件设备(如键盘、显示器、磁盘)以材料形式存在于/dev目录下。例如:/dev/tty表示终端设备。/dev/sda表示第一块硬盘。

文件操作:设备可通过标准文件操作接口(open、read、write、close)访问,用户程序无需关注硬件细节。

      1. 设备管理

字符设备:按字节流访问(如键盘、终端),支撑随机读写。

块设备:按数据块访问(如硬盘、SSD),适合批量传输。

网络设备:通过套接字(socket)接口管理,不直接映射为材料。

8.2简述UnixIO接口及其函数

Unix接口函数的核心思想是通过统一的文件抽象和系统调用访问所有I/O设备,提供简洁、一致的编程接口。

      1. open/close函数——打开/关闭文件或设备,返回档案描述符

int open(const char *pathname, int flags, mode_t mode); 通过flags体现访问模式,mode标注文件权限。

int close(int fd); 释放文件描述符

      1. read/write函数——数据读写

ssize_t read(int fd, void *buf, size_t count);从设备读取数据到缓冲区,返回实际读取的字节数,0表示文件结束,-1表示错误。

ssize_t write(int fd, const void *buf, size_t count); 将缓冲区素材写入设备,返回实际写入的字节数,-1表示错误。

      1. ioctl/lseek——控制与定位

int ioctl(int fd, unsigned long request, ...);执行设备特定控制命令,用于安装设备参数(如设置串口波特率、调整终端窗口大小)。

off_t lseek(int fd, off_t offset, int whence); 用于修改档案偏移量。

      1. 扩展函数与机制

非阻塞I/O通过fcntl(fd, F_SETFL, O_NONBLOCK)设置,读写操作立即返回,避免进程阻塞,在错误处理时,系统调用返回-1时,通过全局变量errno获取错误码;缓冲机制中,标准库(如stdio)在用户层封装缓冲区,通过fflush强制刷新数据到内核。

8.3 Printf的达成分析

printf是用户层格式化输出的核心函数,其实现流程如下:

  1. 格式化字符串处理:

printf("Hello%s",name)调用vsprintf,将参数格式化为字符串(如"HelloAlice")。

处理转义字符、类型转换(如%d转整型)、宽度对齐等。

  1. 系统调用写入:

调用write(1,buf,len),触发syscall或int0x80陷入内核。

  1. 字符显示驱动:

字模库:将ASCII字符转换为像素点阵(如H对应16x16点阵)。

显存(VRAM):存储每个像素的RGB值,地址映射到显卡内存(如0xA0000)。

显示刷新:显卡控制器以固定频率(如60Hz)逐行扫描VRAM,通过信号线输出到显示器。

  1. 关键机制:

用户态到内核态切换:write通过系统调用进入内核,执行设备驱动代码。

内存映射IO:显存通过物理地址直接映射,驱动直接操控内存结束显示。

8.4 Getchar的达成分析

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read平台函数,通过框架调用读取按键ascii码,直到接受到回车键才返回。

getchar借助异步键盘中断实现输入捕获,流程如下:

  1. 键盘中断处理:

用户按键触发键盘控制器中断(IRQ1),CPU调用中断处理程序。

驱动读取键盘扫描码(如0x1C对应回车键),转换为ASCII码(如\n)。

ASCII码存入内核的键盘输入缓冲区(环形队列)。

  1. 系统调用读取:

getchar调用read(0,&c,1),从缓冲区读取字符。

若缓冲区为空,进程阻塞,直到有数据或收到信号。

  1. 返回条件:

读取到回车符(\n)时返回,结束输入。

  1. 关键机制:

异步中断:输入不依赖进程轮询,由硬件中断触发处理。

缓冲区管理:内核维护输入队列,避免数据丢失。

8.5本章小结

Linux的I/O管理基于“一切皆文件”理念,将硬件抽象为设备文件(如终端、磁盘),利用标准接口(open、read、write)统一访问。printf通过系统调用将材料写入显存,由显卡驱动显示;getchar借助键盘中断将输入存入内核缓冲区,read读取至回车符。分层设计隔离复杂度:用户层给予简洁接口(如阻塞/非阻塞模式),内核层通过驱动直接管理硬件(显存映射、中断处理),兼顾效率与安全。结合ioctl等扩展接口,Linux以统一文档模型和分层机制平衡易用性与性能,成为核心特性之一。

结论

“Hello’s P2P”程序的全生命周期研究揭示了计算机系统设计中多层次抽象与软硬件协同的核心思想。从用户编写的C语言源代码到最终在物理硬件上执行的进程,程序经历了预处理、编译、汇编、链接等一系列复杂转换,每一步都体现了系统对效率与灵活性的权衡。预处理阶段经过宏展开与头文件融合为编译器提供纯净的输入,编译阶段则将高级语言逻辑精准映射为平台相关的汇编指令,而链接器凭借符号解析与重定位将分散的目标文件与动态库整合为统一的可执行实体。这一过程中,动态链接机制(如PLT与GOT表)的引入尤为重要,它不仅减少了内存冗余,还依据延迟绑定实现了共享库的高效加载,展现了系统设计中对“按需分配”原则的深刻实践。

Linux平台以“一切皆文件”的抽象模型统一管理异构硬件设备,使得用户程序无需关注底层差异即可做完I/O操作。例如,printf通过标准系统调用将数据传递至内核,由显存映射与显卡驱动完成像素渲染,而getchar则依赖键盘中断与环形缓冲区的异步协作,完成了非阻塞输入的高效处理。这种分层设计不仅简化了开发者的逻辑,还经过权限隔离与内核态保护确保了系统的安全性。在存储管理方面,虚拟内存机制与四级页表的结合为进程提供了独立的地址空间,TLB与多级Cache的协同显著降低了地址转换与材料访问的延迟,而写时复制(COW)科技则在进程fork时优化了内存资源的分配效率,体现了操作系统对物理资源的精细化管控。

程序的执行过程进一步凸显了操作系统的核心作用。进程调度器凭借时间片轮转与优先级策略实现多任务并发,而信号机制(如SIGINT与SIGTSTP)为外部干预提供了标准化接口。异常处理与缺页中断则确保了工具在非法操作或资源不足时的稳健运行。例如,execve通过彻底重构进程的地址空间建立了程序的动态加载,而malloc的堆管理策略与缺页中断的联动则支撑了内存的动态扩展。这些机制共同构建了一个既隔离又协作的执行环境,使得用户程序能够在资源受限的硬件上高效、安全地运行。

面向未来,计算机系统的优化方向可进一步聚焦于实时性增强与安全加固。例如,地址空间布局随机化(ASLR)可抵御恶意代码注入,而容器化技术(如Docker)通过命名空间与控制组(cgroups)深化了资源隔离。此外,随着异构计算架构(如GPU与TPU)的普及,平台设计需探索更灵活的硬件抽象层,以支持多样化的计算范式。对“Hello”程序的剖析不仅深化了对经典系统机制的理解,也为应对新兴技术挑战提供了方法论基础——唯有在分层抽象与硬件协同中不断迭代,才能构建更高效、更安全的计算生态。

附件

文件名

作用说明

hello.c

用户编写的C语言源代码文档,包括程序逻辑和函数实现(如main函数),是编译流程的起点。

hello.i

预处理后的中间资料,包含头文件展开、宏替换后的完整代码(无注释),由gcc -E生成,供编译阶段使用。

hello.s

汇编语言文件,由编译器(gcc -S)将预处理后的C代码转换为目标平台的汇编指令,含有符号引用(如printf@PLT)。

hello.o

可重定位目标文件(Relocatable Object File),由汇编器(gcc -c)生成,包含机器码和未解析的符号地址,需链接器处理。

hello

最终生成的可执行文件,由链接器(gccld)将hello.o与动态库(如libc.so)链接后生成,可直接运行。

pedasssionhello.txt

此档案可能是误输入或额外文件,不在标准编译流程中。

参考文献

  1. Bryant, R. E., & O'Hallaron, D. R. Computer Systems: A Programmer's Perspective (3rd Edition). Pearson, 2015.
  2. Tanenbaum, A. S., & Bos, H. Modern Operating Systems (4th Edition). Pearson, 2014.
  3. Love, R. Linux Kernel Development (3rd Edition). Addison-Wesley, 2010.
  4. GNU Compiler Collection (GCC) Documentation. [Online]. Available:GCC online documentation- GNU Project
  5. Intel® 64 and IA-32 Architectures Software Developer’s Manual. Intel, 2023.
  6. Kerrisk, M. The Linux Programming Interface. No Starch Press, 2010.
  7. [转]printf 函数实现的深入剖析 - Pianistx - 博客园
  8. 深入理解计算机系统 第三版
posted @ 2025-11-24 18:14  gccbuaa  阅读(3)  评论(0)    收藏  举报