摘 要
本文根据《深入理解计算机系统》的各个章节,描述了hello.c从编译到运行,再到进程回收的整个过程,并对执行中的每个步骤都分为一章详细阐述,从而加深对计算机系统的理解。
关键词:深入理解计算机系统;进程;操作系统;虚拟内存
目 录
文章目录
- 第1章 概述
- 第2章 预处理
- 第3章 编译
- 第4章 汇编
- 第5章 链接
- 第6章 hello进程管理
- 第7章 hello的存储管理
-
- 7.1 hello的存储器地址空间
- 7.2 Intel逻辑地址到线性地址的变换-段式管理
- 7.3 Hello的线性地址到物理地址的变换-页式管理
- 7.4 TLB与四级页表支持下的VA到PA的变换
- 每次 CPU 产生一个虚拟地址后,通过它的 VPN 部分看 TLB 中是否缓存,如果命中,直接得到 PPN,将虚拟地址中的 VPO 作为物理页偏移,这样就能得到物理地址;如果 TLB 不命中,则经过四级页表的查找得到最终的PTE,从而得到 PPN,进而得到物理地址。
- 7.5 三级Cache支持下的物理内存访问
- 7.6 hello进程fork时的内存映射
- 7.7 hello进程execve时的内存映射
- 7.8 缺页故障与缺页中断处理
- 7.9动态存储分配管理
- 7.10本章小结
- 第8章 hello的IO管理
- 结论
- 附件
- 参考文献
**
**
第1章 概述
1.1 Hello简介
Hello程序的声明周期是从一个高级C语言程序开始的,为了在系统上运行hello.c程序,每条C语句都必须被其它程序转化为一些列的机器语言指令,最终获得可执行目标文件hello,然后在shell中输入“./hello”,加载并运行可执行目标文件hello,最后回收hello进程。
Hello的P2P过程是从程序文件hello.c开始(Program),由gcc调用cpp,ccl,as,ld,经历了预处理,编译,汇编,链接这一过程(Process),最终获得可执行目标文件hello并保存在磁盘中。
Hello的020是指hello可执行目标文件从运行到最后被回收的过程。当在shell中输入“./hello”时,shell调用fork函数创建一个子进程,然后子进程调用操作系统内核提供的execve函数,将可执行目标文件hello加载到子进程的地址空间,并设置进程上下文。加载器使用mmap函数进行内存映射,将磁盘上的hello文件的内容映射到虚拟内存页中。
1.2 环境与工具
硬件环境:CPU:Intel® Core™ i7-10875H CPU @ 2.30GHz 2.30 GHz,RAM:16.0 GB
操作系统:Windows Subsystem for Linux2(Ubuntu20.04 LTS)
开发工具:vscode、vim、gcc、gdb、edb
1.3 中间结果
列出
文件 | 作用 |
---|---|
Hello.c | 源代码 |
Hello.i | 预处理后的代码 |
Hello.s | 汇编代码 |
Hello.o | 可重定位目标文件 |
Hello.o.elf.txt | Hello.o的ELF |
Hello.o.s | Hello.o反汇编后的代码 |
Hello | 链接后的可执行目标文件 |
Hello.elf.txt | Hello的ELF |
Obj_hello.s | Hello的反汇编代码 |
1.4 本章小结
本章介绍了hello的P2P和020过程,描述了使用的环境与工具,列出了生成的中间文件以及它们的作用。
(第1章0.5分)
**
**
第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理的概念
预处理是编译阶段之前对源代码进行的一系列文本替换和宏扩展操作。这一过程由预处理器cpp完成。
2.1.2 预处理的作用
预处理扫描带有#的命令,并对#语句进行替换。
(1)宏定义和替换
使用#define语句定义宏,然后在代码中使用这些宏,预处理器会将这些宏替换为其定义的内容。比如:
#define PI 3.14
float area = PIradiusradius
在预处理时,PI会被替换为3.14
(2)文件包含
使用#include指令包含其他文件的内容,如头文件。预处理器会将这些文件的内容插入到包含指令的位置。比如:
#include<stdio.h>
在预处理时,stdio.h文件的内容会被插入到代码中。
(3)条件编译
使用 #ifdef、#ifndef、#if、#else、#elif 和 #endif 指令控制代码的编译条件。预处理器会根据这些条件指令决定哪些代码块会被编译。比如:
#ifdef DEBUG
xxxx
#endif
在预处理阶段,如果定义了DEBUG,那么#ifdef 与 #endif之间的语句会被保留;否则,它会被忽略。
(4)宏函数
定义带参数的宏,预处理器会进行相应的替换和展开
#define SQUARE(x) ((x) * (x))
int y = SQUARE(5);
在预处理阶段,SQUARE(5)会被替换为((5) * (5))。
2.2在Ubuntu下预处理的命令
输入命令:gcc -E hello.c -o hello.i
-E表示只激活预处理阶段,最终生成预处理后的文件hello.i
图2-1 hello.i部分内容
2.3 Hello的预处理结果解析
源文件hello.c:
预处理结果hello.i:
(1)外部库文件
首先,开始部分有一系列的外部库.h文件路径
(2)数据类型名称替换
接下来是一些typedef,前面是我们编写代码时使用的标准数据类型,后面的别名就是引入的头文件中使用的类型定义。
(3)内部函数声明
中间部分是很多内部函数的声明,包括系统内核提供的接口的封装:
(4)main函数
最后是我们的main函数代码部分:
2.4 本章小结
本章介绍了hello.c的预处理过程,分析了预处理结果文件hello.i的各部分内容。
可以看到,hello.i文件共有3061行,并且hello.c中的main函数被放在最后。hello.c中的#include<stdio.h>经过预处理后被替换为hello.i中3000多行的stdio.h头文件内容,这体现了预处理简化代码,增加代码可读性和可维护性的作用。可以说,hello.c中还有很多残缺不全的部分,需要经过预处理,将宏定义替换和引用的头文件内容插入到代码中,补全代码,最终得到完整的代码文件hello.i
(第2章0.5分)
**
**
第3章 编译
3.1 编译的概念与作用
编译阶段将预处理的结果文件hello.i转换为汇编代码,汇编器作为这一阶段的核心,对hello.i进行了语法检验,语义分析和优化,最终生成了汇编代码文件hello.s
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
-S表示只激活到编译过程。
3.3 Hello的编译结果解析
接下来对C语言中数据类型及各种操作如何编译到汇编代码进行解析。
3.3.1 常量
(1)字符型常量
hello.c中的printf打印了一个字符串,这个字符串常量存放在.LC0中
(2)其他常量
还有一些常量以立即数出现在汇编代码中,比如if判断中有一个整数5:
它对应的汇编代码:
cmpl 比较argc与5是否相等,如果相等,则跳至.L2,;否则,将调用exit函数退出程序。
3.3.2 变量与运算
(1)局部变量
局部变量存储在寄存器或者栈中。
hello.c中有一个局部变量:
在汇编代码中,int i存放在栈中-4(%rbp)处:
(2)算术运算
在for循环中,局部变量i的值每次加1:
在汇编代码中,这个运算由addl指令完成:
3.3.3 数组/指针操作
main函数的参数中,有一个字符串数组argv:
根据代码:
在汇编代码中,%rdi向sleep函数传递参数,栈中%rbp指向的栈空间存放的内容是argv[4]的地址。
3.3.4 控制转移
for循环中,每次都需要比较i<10,对应的汇编代码是cmpl指令,判断i是否小于等于9,如果是,则继续执行循环体;否则,跳出循环:
3.3.5 函数调用与返回
(1)main函数
传入参数为argc、argv,参数从shell中传入,返回值设置为0
(2)printf函数
设置%rdi和%rsi的值来传入参数:
(3)exit函数
设置%rdi和%rsi的值来传入参数: