0%

程序是怎么运行起来的

GCC的初衷是为GNU操作系统专门编写一款编译器,现已被大多数类Unix操作系统(如Linux、MacOS X等)采纳为标准的编译器,GCC编译C源码有四个步骤:预处理——>编译——>汇编——>链接。

一、流程

C程序运行过程

  1. 预处理器:C/C++的预处理器主要是词法预处理器,进行文本替换、宏展开、删除注释这类操作。

    • gcc -E可以得到预处理的结果,扩展名为.i
    • C/C++预处理不做任何语法检查,语法检查是编译器要做的事情
      • 因为预处理命令不属于C/C++语句,这也是定义宏时不要加分号的原因
    • 预处理之后得到的是真正的源代码
  2. 编译器:对预处理之后的文件进行词法分析、语法分析、语义分析并优化后生成相应的汇编文件,即把文本文件.i翻译成文本文件.s得到汇编语言程序,汇编语言程序中的每条语句都以一种标准的文本格式描述了一条低级机器语言指令。

    • 高级语言翻译为汇编语言
    • gcc -S可以得到编译后的汇编代码,扩展名为.s
      • 汇编语言为不同高级语言的不同编译器提供了通用的输出语言
    • 编译器会进行语法检查
  3. 汇编器:将.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件.o

    • 汇编语言翻译成机器语言
    • gcc -c可以得到汇编后的结果机器代码,扩展名为.o
    • .o是一个二进制文件,它的字节编码是机器语言指令而不是字符
  4. 链接:将独立编译好的片段由链接器链接到一起形成可执行文件(iOS中为*.ipa,Windows中为*.exe,Android中为*.apk)。

    • 静态链接:把库文件的代码全部加入到可执行文件中,其后缀名一般为.a
    • 动态链接:在程序执行时由运行时链接文件加载库,其后缀名一般为.so
    • dyld(dynamic loader and linker)是iOS和Mac OS系统中的动态加载器和链接器,它负责共享库的动态链接,比如libobjc.A.dylibFoundation.framework等,加载和链接是在程序启动时main()函数之前做的。
  5. 装载:把链接后的可执行文件加载到内存。

  6. 运行:CPU执行机器指令。

    • 操作系统在创建进程之后,jmp到这个进程的入口函数
    • 入口函数对程序运行环境进行初始化,包括堆、I/O、线程、全局变量的构造等
    • 入口函数在完成初始化之后,调用main函数,开始执行程序的主体
    • main函数执行完毕之后返回到入口函数,入口函数进行清理工作,最后通过系统调用结束进程

二、例子

  1. 准备,vim test.c
1
2
3
4
5
6
7
#include <stdio.h>
int main(void)
{
char a[20]={'a','b','c'};
printf("%ld\n", sizeof(a));
return 0;
}
  1. 预处理gcc -E test.c -o test.i

  2. 编译gcc -S test.i

  3. 汇编gcc -c test.s

  4. 链接gcc test.o -o test

Linux可通过readelf命令查看ELF文件的头信息,或者动态库文件的导出函数等,Mac下默认无此命令,需要安装brew install binutils,不过即便安装了也不顶用,总提示readelf:错误:不是 ELF 文件 - 它开头的 magic 字节错误,没有做过多的研究。

ELF文件是用于二进制文件、可执行文件、目标代码、共享库和核心转储格式文件。

包含在 ASCII及扩展 ASCII 字符中编写的数据或程序指令的文件。

可执行文件 (executable file) 指的是可以由操作系统进行加载执行的文件。在不同的操作系统环境下,可执行程序的呈现方式不一样。

目标代码(object code)指计算机科学中编译器或汇编器处理源代码后所生成的代码,它一般由机器代码或接近于机器语言的代码组成。

三、参考

  1. 参考一
  2. 参考二
  3. 参考三
  4. 参考四
  5. 参考五