第1章 概述
1.1 Hello简介
P2P过程:Hello最初以ASCII文本形式存在于hello.c文件中,在经过预处理,编译,汇编,链接过程后,将创建进程,利用fork函数和execve函数创建子进程,加载可执行文件并替换地址空间。
020过程:从零开始,从磁盘中加载,进行初始化后进入指令周期,并被系统调用,进行I/O管理后归于零。
1.2 环境与工具
在编写本论文过程中,软件环境为Vmware17 Pro,使用Visual studio 2022 进行开发,GCC工具进行调试。
1.3 中间结果
hello.i: 预处理后得到的文本文件
hello.s: 编译后得到的汇编语言文件
hello.o: 汇编后得到的可重定位目标文件
hello.elf: hello.o的elf格式
hello:链接后得到的可执行目标文件
Hello.elf:hello的elf格式
Hello.asm: 反汇编hello得到的反汇编文件
1.4 本章小结
本章介绍了hello程序的P2P 020过程,并介绍了在完成本论文过程中所使用的软硬件环境,开发调试工具。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理是指编译器在正式编译之前,由预处理器(cpp)对源代码进行的文本处理阶段。它不分析代码的语法结构,而是进行纯文本替换和操作。
作用:将头文件内容插入源文件,定义常数或函数式宏,控制代码编译范围
2.2在Ubuntu下预处理的命令
预处理命令如下:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
对比hello.c和hello.i,可知程序被处理后体积显著膨胀,在头文件中,通过studio.h引入了完整的标准输入输出体系,包含呈现典型的树状结构,形成五级嵌套的声明网络。原函数中的所有宏定义,均被实际的值替换。
2.4 本章小结
本章介绍了预处理的作用,并成功得到了hello.c的预处理文件。
第3章 编译
3.1 编译的概念与作用
概念:此处阶段的编译特指将预处理后的源代码(.i文件)转换为汇编代码(s文件)的过程
作用:将高级语言的抽象逻辑转换为底层硬件相关的汇编指令,删除冗余计算,根据目标CPU架构生成对应的指令集。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1数据类型处理分析
(1)字符串常量处理
– 编译器将字符串常量存储在.rodata只读数据段
– 中文字符被转换为UTF-8编码的八进制序列
(2)指针与数组处理
– 命令行参数`char *argv[]`被转换为二级指针:
movq %rsi, -32(%rbp)
– 数组访问被优化为指针运算:
addq $8, %rax
movq (%rax), %rsi
(3)整型变量处理
– 循环变量i(4字节int)存储在栈帧-4(%rbp)处
– 使用32位寄存器操作:
addl $1, -4(%rbp)
cmpl $9, -4(%rbp)
3.3.2关键操作实现
(1)算术运算
– 指针运算保持原始数据类型宽度:
addq $24, %rax
– 整型运算使用对应位宽指令:
addl $1, -4(%rbp)
(2)控制流实现
– 条件跳转使用比较+条件跳转指令对:
cmpl $5, -20(%rbp)
je .L2
– 循环结构转换为底部判断模式:
.L3:
cmpl $9, -4(%rbp)
jle .L4
3.3.3系统调用封装
(1)库函数调用
使用PLT(过程链接表)实现动态链接:
call printf@PLT
call sleep@PLT
atoi转换显示栈操作:
movq (%rax), %rdi
call atoi@PLT
movl %eax, %edi
3.3.4 完整数据流示例
C代码片段:
printf("Hello %s %s %s\\n",argv[1],argv[2],argv[3]);
对应汇编实现:
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rsi
addq $16, %rax
movq (%rax), %rdx
addq $24, %rax
movq (%rax), %rcx
leaq .LC1(%rip), %rd
call printf@PLT
该实现展示了编译器如何:
1. 维护指针的正确偏移计算
2. 按调用约定安排参数寄存器
3. 处理可变参数函数的特殊要求
3.4 本章小结
本章介绍了编译的概念与作用,得到了hello.i编译后的文件hello.s,并对其进行了解析。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编是将汇编语言程序转换为机器语言二进制程序的过程,由汇编器完成。这一过程是编译流程中的关键步骤,实现从人类可读的助记符到计算机可执行指令的最终转换。
作用:将汇编代码转换为机器代码,记录标签,函数名等生成符号链表供连接器使用,标记需要在链接阶段确定的地址,将代码、数据分类储存在目标文件的特定节区。
4.2 在Ubuntu下汇编的命令
指令:gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
利用指令readelf -S hello.o分析hello.o的ELF格式,结果如下:
节区编号 名称 类型 大小 标志 对齐 作用
[1] .text PROGBITS 0xa3 AX 1 存储程序机器指令
[3] .data PROGBITS 0x0 WA 1 已初始化全局数据
[4] .bss NOBITS 0x0 WA 1 未初始化全局数据
[5] .rodata PROGBITS 0x40 A 8 只读数据
节区编号 名称 类型 大小 链接目标 作用
[2] .rela.text RELA 0xc0 11→.symtab 代码段重定位条目
[10] .rela.eh_frame RELA 0x18 11→.symtab 异常帧重定位条目
[11] .symtab SYMTAB 0x108 12→.strtab 符号表
[12] .strtab STRTAB 0x32 符号名称字符串表
重定位类型:
类型名 触发场景 修正方式
R_X86_64_PLT32 调用外部函数 替换为PLT条目相对地址
R_X86_64_PC32 访问.rodata中的字符串 计算目标与PC的偏移量 |
R_X86_64_32 绝对地址引用 直接替换为符号最终地址
4.4 Hello.o的结果解析
以下是对helllo.o进行反汇编的结果
可见该结果与hello.s中使用的汇编代码是一样的,但是夹杂了一些机器代码。每一条汇编语言都可以用机器二进制数据来表示,汇编语言中的操作和操作数和机器语言进行一一对应,使机器能够理解代码的含义并进行执行。
4.5 本章小结
本章介绍了从hello.s到hello.o的过程,解释了hello.o的ELF形式,并对hello.o进行了反汇编以进行解释,找到机器代码和汇编代码的区别。
第5章 链接
5.1 链接的概念与作用
概念:由编译器将源代码编译生成的中间文件,包含机器代码和符号表。
作用: 将多个目标文件和所需的库合并,解析它们之间的符号引用,生成一个完整的、可加载执行的文件。
5.2 在Ubuntu下链接的命令
命令为
ld-ohello-dynamic-linker/lib64/ld-linux-x86-64.so.2/usr/lib/x86_64-linux-gnu/crt1.o/usr/lib/x86_64-linux-gnu/crti.ohello.o/usr/lib/x86_64-linux-gnu/libc.so/usr/lib/x86_64-linux-gnu/crtn.o
5.3 可执行目标文件hello的格式
对hello文件的ELF头进行分析:
查看所有段:
5.4 hello的虚拟地址空间
在gdb中查看:
关键段:0x400000-0x401000:代码段(对应readelf -l 中的LOAD段)。
0x600000-0x601000:只读数据段。
0x601000-0x602000:可读写数据段。
[heap]和 [stack]:动态分配的堆和栈空间。
5.5 链接的重定位过程分析
得到的结果如下:
hello.o中,符号地址并未全部解析,存在重定位项,无动态链接信息,而在hello中,符号地址已解析为实际虚拟地址,重定位项已处理,存在动态链接信息
5.6 hello的执行流程
入口地址
main函数地址
终止点地址
5.7 Hello的动态链接分析
未运行时观察PLT项:
0x404020(GOT)初始指向PLT中的解析例程。
运行后观察GOT项的变化
5.8 本章小结
本章介绍了链接的作用,解释了程序如何进行重定位的操作,说明了链接的工作原理。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是程序的执行实例,是操作系统进行资源分配和调度的基本单位。每个进程拥有独立的地址空间、文件描述符、寄存器状态等资源。
作用:资源隔离、并发执行、系统管理
6.2 简述壳Shell-bash的作用与处理流程
作用:是用户与操作系统内核的交互接口,解释并执行用户输入的命令。
流程:1. 读取输入:通过`readline库获取用户命令。
2. 解析命令:分割为命令名和参数。
3. 执行命令:
内置命令直接执行。
外部程序:通过fork()创建子进程,execve()加载程序。
4. 等待结果:父进程(Shell)通过waitpid()等待子进程结束。
6.3 Hello的fork进程创建过程
1. 调用fork():
Shell调用fork()创建子进程,复制父进程的PCB、文件描述符等。
子进程获得独立的PID,返回值为0(父进程返回子进程PID)。
2. 内存复制:
采用写时复制(Copy-On-Write)技术,初始共享物理内存,修改时再复制。
3. 执行hello:
子进程调用execve()加载hello的代码段和数据段,替换原有内存空间。
6.4 Hello的execve过程
1. 参数传递:
Shell将argv[]传递给execve。
2. 加载程序:
– 内核读取ELF头部,映射代码段和数据段到内存。
– 动态链接器析共享库依赖。
3. 设置入口点:
跳转到_start(程序入口),最终调用main()。
6.5 Hello的进程执行
1. 时间片分配:
默认时间片10ms,通过时钟中断触发调度。
2. 用户态→内核态:
发生系统调用或中断时,CPU切换到内核态。
3. 上下文切换:
保存当前进程寄存器状态到PCB,加载下一个进程的PCB。
6.6 hello的异常与信号处理
Ctrl+Z,发送一个SIGTSTP信号,导致挂起进程
Ctrl+C,发送一个SIGINT信号,导致进程终止
回车,会导致进程被读取
6.7本章小结
在本章中,探讨了进程管理的实现方式,并且了解了shell如何进行信号的处理,理解了shell是如何作用的。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1. 逻辑地址:
程序员可见的地址。
2. 线性地址:
现代Linux默认平坦模式,逻辑地址直接作为线性地址。
3. 虚拟地址:
用户态看到的地址,通过页表映射到物理地址。
4. 物理地址:
由MMU通过页表转换得到。
7.2 Intel逻辑地址到线性地址的变换-段式管理
1. 段选择子:
16位寄存器(CS/DS等)指向GDT(全局描述符表)条目。
2. 段描述符:
包含段基址和界限。
3. 线性地址计算:
线性地址 = 段基址 + 逻辑地址偏移量
所有段基址设为0,逻辑地址=线性地址)。
7.3 Hello的线性地址到物理地址的变换-页式管理
1. 虚拟地址划分:
[47:39] PML4索引 → [38:30] PDPT索引 → [29:21] PD索引 → [20:12] PT索引 → [11:0] 页内偏移
2. 查表过程:
CR3寄存器指向PML4表基址,逐级索引找到物理页框号。
3. 物理地址生成:
物理地址 = PFN × 4KB + 页内偏移
7.4 TLB与四级页表支持下的VA到PA的变换
1. TLB查找:
用虚拟地址高位作为Tag查询TLB,命中则直接获取PFN。
2. TLB未命中:
遍历四级页表,更新TLB缓存。
7.5 三级Cache支持下的物理内存访问
1. CPU访问0x7fa030(物理地址)。
2. 依次查询L1→L2→L3 Cache,未命中则访问DRAM。
3. 缓存行大小通常为64B,对齐访问提升效率。
7.6 hello进程fork时的内存映射
1. 共享物理页:
父进程和子进程的页表项指向相同的物理页,标记为只读。
2. 触发复制:
任一进程尝试写入时,触发缺页异常,内核分配新物理页并复制内容。
7.7 hello进程execve时的内存映射
1. 释放旧空间:
清除非共享的内存区域(堆、栈等)。
2. 加载新程序:
映射hello的代码段到0x400000,数据段到0x601000。
3. 动态链接
加载ld-linux和libc.so到共享库区域。
7.8 缺页故障与缺页中断处理
1. 异常触发:CPU陷入内核态,保存上下文。
2. 查VMA:确认地址合法性。
3. 分配物理页:
文件映射:从磁盘读取(如代码段);
匿名映射:清零新页(如堆内存)。
4. 更新页表:建立虚拟→物理映射,返回用户态继续执行。
7.9本章小结
本章讨论了地址转换,内存映射,动态内存等一系列的问题,有助于了解程序运行的本质。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
Linux将所有IO设备抽象为文件,通过文件系统统一管理。设备文件存放在/dev目录下,分为:
字符设备(如键盘):按字符流传输,支持顺序访问。
块设备如磁盘):按数据块传输,支持随机访问。
设备管理:Unix IO接口
通过统一的文件操作接口管理设备,屏蔽硬件差异,实现“一切皆文件”的哲学。
8.2 简述Unix IO接口及其函数
Unix IO接口提供以下核心函数:
1. open:打开设备文件,返回文件描述符。
int open(const char *pathname, int flags);
2. read:从设备读取数据。
ssize_t read(int fd, void *buf, size_t count);
3. write:向设备写入数据。
ssize_t write(int fd, const void *buf, size_t count);
4. close:关闭设备文件。
int close(int fd);
5. ioctl:控制设备的特定功能(如设置串口波特率)。
int ioctl(int fd, unsigned long request, …);
8.3 printf的实现分析
1. 格式化字符串:
printf调用vsprintf,将可变参数按格式字符串转换为字符串,存入缓冲区。
2. 系统调用:
调用write系统函数,通过陷阱指令进入内核态。
3. 字符显示驱动:
– 从ASCII码查询字模库(如位图字体),生成像素点阵。
– 将像素信息写入显存(VRAM),存储每个像素的RGB值。
4. 屏幕刷新:
显示芯片按刷新频率逐行读取VRAM,通过信号线输出到显示器。
8.4 getchar的实现分析
1. 键盘中断:
按键触发硬件中断,CPU跳转到键盘中断处理程序。
2. 扫描码转换:
中断程序将键盘扫描码转换为ASCII码,存入内核缓冲区。
3. 系统调用:
getchar调用read系统函数,从缓冲区读取ASCII码:
– 若缓冲区为空,则阻塞等待输入。
– 遇到回车符时返回数据。
8.5本章小结
本章探讨了Linux系统的IO设备管理办法,Linux系统通过文件抽象IO设备,简化管理,并解释了两种函数的底层逻辑。
结论
Hello程序在计算机系统中的执行过程
从源代码到屏幕输出,hello程序经历了以下关键步骤:
1. 源代码编译
预处理:
#include <stdio.h>等指令被展开,生成纯C代码。
编译:
编译器将C代码转换为汇编代码,仍为人类可读文本。
汇编:
汇编器将汇编代码转换为机器指令,生成可重定位目标文件。
2. 链接
链接器合并hello.o与libc.so,解析符号引用,生成可执行文件。
关键点:动态链接库在运行时加载,静态链接库直接嵌入可执行文件。
3. 加载与执行
Shell调用execve:
Shell通过fork创建子进程,子进程调用execve加载a.out,替换为hello的代码段、数据段。
虚拟内存映射:
操作系统为进程分配虚拟地址空间,映射代码、数据、堆栈等段(通过页表管理物理内存)。
4. 运行
printf的执行:
1. 调用vsprintf格式化字符串,存入用户态缓冲区。
2. 触发write系统调用,切换到内核态。
3. 内核检查文件描述符,调用显示驱动。
5. 显示输出(
字符渲染:
1. 驱动从字模库获取ASCII对应的像素矩阵,写入显存(VRAM)。
2. 显卡控制器逐帧扫描VRAM,通过视频信号(如HDMI)输出到显示器。
计算机系统设计与实现的深切感悟
1. 计算机系统的分层抽象, 从高级语言到机器指令,每一层隐藏底层细节。
2. 硬件与软件的协同,硬件为操作系统提供基础功能。
3.安全与隔离,进程间通过虚拟内存隔离,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.



