一个简单的hello,却有着不平凡的人生。本文以hello为例,讲述了Linux下从C文件到可执行文件,从程序到进程,最后到程序循环结束的整个过程。其中我们将介绍关于预处理、编译、汇编、链接、进程管理、存储管理的各种细节。这些都有赖于计算机系统硬件和软件的共同支持,在所有系统部件的协调工作下,一个简单的应用程序也显示出非凡高效的结构和流程。
第1章 概述
1.1 Hello简介
Hello程序将从hello.c文件,经历编译,创建进程,进程在时间片上运行等环节,最终完成自己的使命。我们首先将简要介绍Hello程序所经历的几个环节的基本情况。
从代码到可执行文件:hello.c文件经过gcc编译器预处理以后成为hello.i文件,经过编译和汇编得到一个孤立的可执行程序hello.o,这个程序尚未建立和系统中其他共享库和代码段之间的联系。最后我们显示调用链接指令,得到一个完整的,每一行代码都有明确定义的行为的一个可执行文件hello.out。
从可执行文件到进程:万事具备只欠东风。我们通过在bash中调用hello.out脚本,使得bash将其fork出的一个子进程的虚拟内存空间设置成hello.out文件中描述的内容。这些虚拟内存空间表示为指向磁盘某一位置的VA,并被MMU交换到物理内存中,使得CPU可以快速访问。至此“P2P”的过程就完成了。
进程从运行到停止:内核不断地给hello.out进程分配时间片,在时间片中CPU执行hello.out的只读代码区域包含的指令,直到进程收到中断/停止信号,或者进程结束自然退出。退出后hello.out进程对应的内存片段不断成为其他进程的牺牲片段,直到hello.out程序的内容彻底离开内存,完成了“O2O”的循环。
1.2 环境与工具
软件环境:
操作系统:macOS Monterey 12.3 64位
Bash环境:zsh
终端程序:iTerm2
C编译器:clang/clang++
代码编辑器:Visual Studio Code
P.S. 第三章编译过程在一台CentOS服务器上完成,系统信息:
Linux version 4.18.0-193.28.1.el8_2.x86_64
(gcc version 8.3.1 20191121 (Red Hat 8.3.1-5) (GCC))
硬件环境:
MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports)
处理器:2 GHz 四核Intel Core i5
内存和储存器:16 GB 3733 MHz LPDDR4X,500GB闪存
1.3 中间结果
包括的文件如下:
- hello.c:hello源文件
- hello.i:预处理文件
- hello.s:hello编译出的汇编代码
- hello.o:未链接的汇编结果
- hello_elf.txt:hello.o的ELF格式
- hello_elf_final.txt:hello的ELF格式
- hello_reverse.txt:hello.o的反汇编结果
- hello_rev_final.txt:hello的反汇编结果
1.4 本章小结
本章描述了hello程序完成O2O过程的三个主要环节和完成本次大作业使用的软硬件环境。
第2章 预处理
2.1 预处理的概念与作用
预处理从一个c文件产生一个新的c文件(以.i结尾)。其不对代码中的具体函数和语句进行处理,预处理处理的是c文件中的“宏”,即以#开头的语句,如#define,#include,#if等。同时预处理也会删除所有的注释
例如,对于#include<filename>,预处理程序会用filename中的代码代替c文件中的#include语句。相当于将filename文件的内容插入到c文件中。而对于#define A B,则会把c文件中的所有B文本替换成A。
2.2在Ubuntu下预处理的命令
在Ubuntu和macOS下,都可以使用gcc -E input_file.c来实现预处理。
2.3 Hello的预处理结果解析
查看hello.i文件,可以看到其包含两千余行。其中的内容可以分为三类:
- hello.c文件内容。这部分内容中的宏都被适当的处理了,由于原hello.c文件中没有宏,因此这部分和原文件中内容一致。这些处理后的内容位于i文件的末尾。
- #include宏明确包含的文件。#include文件中的内容会被插入到c文件的对应位置,如图:hello.i中关于printf的声明语句实际上就来自stdio.h文件。
- 关于行号的注释:可以看到i文件中包含很多以#开头的语句,这些语句包含了关于被include的文件的相关信息,有助于debugger显示有用的追踪信息。
2.4 本章小结
这一章展示了编译器预处理的过程,预处理文件实际上也是合法的c文件。预处理只是对c语言中的宏进行了处理,新的i文件是一个不包含宏的文件。方便后续编译器将c语言代码转化成为汇编语言。
第3章 编译
3.1 编译的概念与作用
编译指编译器接受c语言文件,产生对应的汇编语言文件。汇编语言只需要汇编和链接就可以得到可以执行的二进制文件。汇编包含不同的优化等级,包括-Og,-O1,-O2,-O3等等。不同编译选项会产生不同的汇编语言文件,更高的编译优化等级相应的执行得也更快。
3.2 在Ubuntu下编译的命令
在CentOS下使用命令gcc -S -Og -fno-asynchronous-unwind-tables hello.i -o hello.s来进行编译。使用-fno-asynchronous-unwind-tables的目的是减少不必要的仅对编译器可见的语句(Call Frame Information)
3.3 Hello的编译结果解析
执行编译指令后,我们得到了hello.s汇编语言文件。对该文件进行简单的注释:(见这一节最末尾)首先我们对几个重要的方面进行分析
3.3.1表示变量
hello.c中包含常数变量(e.g. "用法: Hello 学号 姓名 秒数!\n")和局部变量(e.g. int i)。它们在汇编中的表示是不一样的。
对于常数变量,在这里表现为字符串产量,hello.c中的字符串常量被放在text段中,并标注为只读状态(rodata)。
对于局部变量(main开头定义的i),汇编语言则使用寄存器%ebx代表其值。
3.3.2获得函数参数
main函数含有两个参数int argc, char *argv[],这些元素保存在函数栈中,大致按照以下方式:
因此汇编代码中,对各个参数的访问如下表,假定运行命令为 ./hello 潘文博 7203610819 3
C变量 |
汇编访问方式 |
具体值 |
int argc |
%edi |
4 |
char **argv |
%rbp |
栈顶地址 |
char *arg[0] |
(%rbp) |
./hello |
char *arg[1] |
8(%rbp) |
“潘文博” |
char *arg[2] |
16(%rbp) |
“7203610819” |
char *arg[3] |
24(%rbp) |
“3” |
3.3.3处理if语句
hello.c中的if语句由汇编文件的第17行到第26行表达:
<