程序人生-Hello’s P2P

摘 要
本文介绍了在Linux操作系统下hello的整个生命周期。借助gcc,objdump等工具,对hello的预处理、编译、汇编、链接等过程进行分析。并对程序hello运行过程中的动态链接库调用、内存管理、系统级I/O等进行介绍。
关键词:预处理;编译;汇编; 链接;进程;内存管理;IO;

目 录

第1章 概述 - 4 -
1.1 Hello简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 5 -
1.4 本章小结 - 5 -
第2章 预处理 - 6 -
2.1 预处理的概念与作用 - 6 -
2.2在Ubuntu下预处理的命令 - 6 -
2.3 Hello的预处理结果解析 - 6 -
2.4 本章小结 - 7 -
第3章 编译 - 8 -
3.1 编译的概念与作用 - 8 -
3.2 在Ubuntu下编译的命令 - 8 -
3.3 Hello的编译结果解析 - 8 -
3.4 本章小结 - 14 -
第4章 汇编 - 15 -
4.1 汇编的概念与作用 - 15 -
4.2 在Ubuntu下汇编的命令 - 15 -
4.3 可重定位目标elf格式 - 15 -
4.4 Hello.o的结果解析 - 19 -
4.5 本章小结 - 20 -
第5章 链接 - 21 -
5.1 链接的概念与作用 - 21 -
5.2 在Ubuntu下链接的命令 - 21 -
5.3 可执行目标文件hello的格式 - 21 -
5.4 hello的虚拟地址空间 - 26 -
5.5 链接的重定位过程分析 - 27 -
5.6 hello的执行流程 - 29 -
5.7 Hello的动态链接分析 - 30 -
5.8 本章小结 - 30 -
第6章 hello进程管理 - 31 -
6.1 进程的概念与作用 - 31 -
6.2 简述壳Shell-bash的作用与处理流程 - 31 -
6.3 Hello的fork进程创建过程 - 31 -
6.4 Hello的execve过程 - 32 -
6.5 Hello的进程执行 - 33 -
6.6 hello的异常与信号处理 - 33 -
6.7本章小结 - 35 -
第7章 hello的存储管理 - 36 -
7.1 hello的存储器地址空间 - 36 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 36 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 37 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 42 -
7.5 三级Cache支持下的物理内存访问 - 43 -
7.6 hello进程fork时的内存映射 - 44 -
7.7 hello进程execve时的内存映射 - 45 -
7.8 缺页故障与缺页中断处理 - 46 -
7.9动态存储分配管理 - 47 -
7.10本章小结 - 50 -
第8章 hello的IO管理 - 51 -
8.1 Linux的IO设备管理方法 - 51 -
8.2 简述Unix IO接口及其函数 - 51 -
8.3 printf的实现分析 - 52 -
8.4 getchar的实现分析 - 53 -
8.5本章小结 - 54 -
结论 - 14 -
附件 - 15 -
参考文献 - 16 -

第1章 概述
1.1 Hello简介
P2P:C语言源代码文件(Program)hello.c被GCC编译器的驱动程序读取,通过四个步骤被翻译为可执行文件hello:
预处理阶段:C预处理器根据#include命令和#define声明拓展源代码,生成了另一个C程序hello.i;
编译阶段:编译器(ccl)产生源文件的汇编代码hello.s;
汇编阶段:接下来,汇编器(as)会将汇编代码hello.s转化成二进制目标代码hello.o;
链接阶段:最后,链接器(ld)将目标代码文件hello.o与实现库函数的代码合并,并生成最终的可执行代码文件hello。
之后执行该文件会将该文件加载在内存中,由系统执行。这时,在shell中运行它,OS(进程管理)会通过fork来为其创建一个新的进程(Process)。
这样就完成了P2P(From Program to Process)过程。

O2O:可执行文件hello运行时shell会为其分配对应的虚拟内存空间。在开始运行进程的时候分配并载入物理内存,开始执行hello的程序,在CPU的帮助下,它的指令被一步步执行,成功在屏幕上输出程序运行完成后系统会回收hello进程并且删除内存中对应的数据,完成O2O(From Zero-0 to Zero-0)过程。
1.2 环境与工具
1.2.1 硬件环境
联想拯救者y7000
CPU: Intel®_Core™i7-9750HF_CPU@_2.60GHz
内存(RAM): 16GB
硬盘: WDC WDS100T2B0C-00PXH0
1.2.2 软件环境
Windows 10 家庭中文版
Ubuntu 18.04 LTS 64位
1.2.3 开发工具
gcc (GCC) 9.2.0
GNU Make 4.2.1
GNU Emacs 26.3
1.3 中间结果
hello.i 预处理后修改了的源程序
hello.s 汇编生成的hello的汇编程序
hello.o 编译生成的hello的可重定位目标程序
hello 链接生成的hello的可执行目标程序
asm.txt hello.o的反汇编文件
1.4 本章小结
本章简述了hello的P2P,O2O过程,介绍了编写本文时的工作环境。

第2章 预处理
2.1 预处理的概念与作用
概念:预处理就是预处理器根据#标识的命令(头文件、宏定义、条件编译等),修改原始c代码,将包含的头文件插入到c代码中,并将宏定义进行替换,去除注释等,形成一个.i文本文件。
作用:1.程序的预处理过程就是将预处理指令(可以简单理解为#开头的正确指令)转换为实际代码中的内容(替换) 2.#include,这里是预处理指令,包含头文件的操作,将所包含头文件的指令替代 3.如果头文件中包含了其他头文件,也需要将头文件展开包含
2.2在Ubuntu下预处理的命令
cpp hello.c > hello.i

图21 hello.c的预处理
2.3 Hello的预处理结果解析

图2-2 hello.c

图2-3 hello.i
可以观察到,预处理后文件变大了很多,原本的#include全部消失,变为stdio.h等头文件的代码。但主体代码缺没有任何改变。
2.4 本章小结
本章节简单介绍了c语言在编译前的预处理过程,简单介绍了预处理过程的概念和作用,对预处理过程进行演示,并举例说明预处理的结果还有解析预处理的过程。

第3章 编译
3.1 编译的概念与作用
编译:即编辑器将某种编程语言转化为汇编代码的过程。在本例中就是将hello.i转化为hello.s。
作用:将高级语言源程序翻译成等价的目标程序,并且进行语法检查、调试措施、修改手段、覆盖处理、目标程序优化等步骤。
3.2 在Ubuntu下编译的命令
gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s

图3-1 编译指令
3.3 Hello的编译结果解析
3.3.1.数据
本例中,hello.c中数据类型有:整型,字符串和数组。
3.3.1.1.整型
C程序的全局变量,已初始化的存放在.data节,未初始化的或初始化为0的存放在.bss节。hello中没有全局变量。
对于局部变量,程序要么存在寄存器中,要么存在用户栈中,函数返回时恢复栈帧。例如,在hello.s中,循环变量i采用了存放在用户栈中的方法(如图3-2所示),-4(%rbp)就是循环变量i。

图3-2 临时变量i
3.3.1.2.字符串
hello.c中有两个字符串,即:
1.printf(“Usage: Hello 学号 姓名!\n”);
2.printf(“Hello %s %s\n”, argv[1], argv[2]);
字符串都声明在.section与.rodata中,在hello.s中以如下形式出现:

图3-3 字符串在汇编代码的存储
3.3.1.3.数组
main函数中的第二个参数char *argv存储在栈上。在汇编代码中,存放在-32(%rbp)。

图3-4 argv在汇编代码的体现
3.3.2.赋值
赋值操作一般用movq指令实现。如hello.s中,就将循环变量i的初始值设为0(图3-3的第二个红框)。
已初始化全局变量的初始值直接保存在.data段内,无需mov指令。
3.3.3.算术操作
常见的算数操作指令如图3-5所示:

图3-5 算数操作汇编指令

hello.s中只有一处算数操作,即i++运算。在汇编代码中用addl指令实现。

图3-6 算数操作汇编指令
hello.s中无逻辑运算操作。
3.3.4.关系操作
hello.c中有用到的关系操作有:“!=”和“<”。
(1)“!=”运算 比较通过cmp来实现,指令根据两个操作数之间的差值来设置条件码。如果两个操作数相等,则标记条件码ZF=1,表示两个数是相等的。如果第一个操作数比第二个小,则设置条件码SF=1,表示比较结果为负数,计算机会根据这些条件码来决定跳转。所以“!=”通过如下代码实现:

图3-7 不等语句在汇编代码的体现
(2)“<”运算 类比与“!=”实现:

图3-8 小于语句在汇编代码的体现
3.3.5.数组操作
hello.c中,argv是char*型的数组。argv作为hello的第二个参数,其首元素地址存放在寄存器%rdi中,之后被放进栈空间中的-32(%rbp)位置。引用数组元素时,用“基地址加偏移量”的方式寻址,如图3-4所示。

图3-9 hello中argv数组的引用方式
3.3.6.控制转移
第一处是判断argv是否等于3,若不等于,则继续执行,若等于,则跳转至L2处(循环前对i初始化)继续执行。

图3-10 第一处控制转移
第二处是对i初始化为0后的无条件跳转,以跳到L4,即循环部分代码。

图3-11 第二处控制转移
第三处是判断是否达到循环终止条件(i<10),这里用i与9进行比较,若小于等于则跳回L4重复循环,否则执行循环外的下一步。这里将i<10的比较改为了与其等价的i<=9。

图3-12 第三处控制转移
3.3.7.函数操作
hello.c中涉及函数main,printf,exit,sleep,atoi和getchar。
(1)main函数
main是整个程序的入口,系统传入参数argc,argv,通过寄存器%rdi(%edi)和%rsi传入,在开始时将这两个参数圧入栈中便于使用寄存器,由系统调用。如下:

图3-13 main函数将参数压入栈在汇编代码的体现
最后main函数返回值为0,通过%rax(%eax)。

图3-14 main函数返回语句在汇编代码的体现
(2)prinft函数
第一次调用及其汇编代码:

图3-16 第一次调用printf语句及其汇编代码
其中$.LC0就是字符串"Usage: Hello 学号

你可能感兴趣的:(程序人生)