从C语言编译看高级程序语言执行
1. C语言编译过程
编译过程流程图:
1.1. 预处理文本(Preprocessing)
解析源码文件文件中的宏指令,将源码转换为更详细的源码,对于文件main.c:
#include<main.h>
int main(){
return 0 ;
}
定义main.h:
int add(int a, int b);
进行预处理:
gcc -E -I . main.c
参数-E
含义:
-E Only run the preprocessor
参数-I
含义:
-I
Add directory to include search path
输出结果内容:
# 1 "main.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 361 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "main.c" 2
# 1 "./main.h" 1
int add(int a, int b);
# 2 "main.c" 2
int main(){
return 0 ;
}
预处理后的内容把#include<main.h>
替换成了main.h中的代码。
1.2. 编译成汇编(Assemble)
将预处理后的文件编译成汇编文件:
gcc -I . -S main.c
生成文件main.s:
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 13
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
xorl %eax, %eax
movl $0, -4(%rbp)
popq %rbp
retq
.cfi_endproc
## -- End function
.subsections_via_symbols
参数 -S
:
-S Only run preprocess and compilation steps
1.3. 生成目标文件
目标文件也是机器码,但是没有运行库,不能执行:
gcc -c main.s -o main.o
文件内容main.o:
����� 8p�p__text__TEXT��__compact_unwind__LD �@__eh_frame__TEXT0@
h$
HX
PUH��1��E�]�zRx
_main �$��������A�C
因为main.s是汇编文件,也可以使用汇编的编译方法:
as main.s -o main.o
1.4. 链接运行库,生成二进制
给目标文件链接上运行库和OS信息,变成可以执行文件:
gcc main.o main
可执行文件因为有了运行库,所以就比main.o文件大多了。
注意main文件不仅仅有机器指令,还有操作系统的信息,否则windows/linux的应用程序可以兼容了。
1.5. gcc 编译过程产生的文件
-
*.c, *.h
源码文件 -
*.i
预处理后的文件 -
*.s
汇编文件 -
*.o
机器码文件,可能是链接前的目标文件,也可能是链接后的可执行文件
文件之间的关系:
上述案例中产生的文件大小对比:
❯ ls -l
total 56
-rwxr-xr-x 1 wuhf staff 4248 6 13 10:48 main # main.o和运行库链接生成的可执行文件,比main.o大
-rw-r--r--@ 1 wuhf staff 45 6 13 01:11 main.c
-rw-r--r-- 1 wuhf staff 23 6 13 01:11 main.h
-rw-r--r-- 1 wuhf staff 211 6 13 10:55 main.i # 比 main.c + main.h 还大,是拼接到一起的
-rw-r--r-- 1 wuhf staff 608 6 13 10:47 main.o # 由 main.s 编译生成的目标文件
-rw-r--r-- 1 wuhf staff 485 6 13 10:37 main.s # 由main.i 生成的汇编文件
1.6. gcc 编译器干了什么
程序必须变成机器码才能被CPU执行,不管这个程序是OS还是应用。
gcc的最终目标是将txt的源码文件变成可以被硬件CPU识别的机器指令。
但是gcc并没有直接把txt变成机器指令,而是翻译成了汇编指令,汇编指令又转换为可以被机器识别的指令。
所以gcc 最核心的功能就是把txt的源码翻译汇编指令。
1.7. gcc 参数
-S Only run preprocess and compilation steps
-E Only run the preprocessor
-I
Add directory to include search path
2. 验证上述分析的可靠性
建立一个hello.c内容:
#include<stdio.h>
int main(){
printf("Hello World!");
}
分步编译:
❯ gcc -S hello.c
❯ as hello.s -o hello.o
❯ gcc hello.o -o hello
❯ ./hello
Hello World!
直接将c文件编译成可执行程序:
❯ gcc hello.c -o hello
❯ ./hello
Hello World!
3. C/Java/Python 程序执行方式对比
3.1. C
gcc 把c文件翻译成汇编指令,再把汇编指令编译成机器指令执行,C文件变成了机器可执行的文件。
3.2. Java
java编译器的主要工作是把java源码文件转换为符合jvm规范的class文件;
jvm的主要工作是执行class文件,把class文件中的指令翻译成不同操作系统的函数调用。
java与C之间明显的区别是java程序的可执行程序就是java.exe,换句话说java的源码没有(不代表不能)被编译成机器指令。
问:Java 为什么不直接编译成机器指令?
答: 编译成机器指令会携带操作系统的信息,导致编译出来的程序不能跨平台使用。
问:class能否变成机器指令?
答: 如果class文件能变成机器指令,那么java既可以使用class来实现跨平台,一次编译到处运行,也可以实现更好的性能。
但是这样做的弊端是执行class前需要再进行一次编译,也是耗时的。
jvm本身就是C/C++程序,某种程度讲JAVA就是C的一个高级API,所以性能不比C落后太多,所以每次运行前编译得不偿失。
Java有JIT
技术可以在运行时把把特定代码编译成机器指令。
3.3. Python
python是脚本语言,所以它不需要编译器,执行python的是它的解释器python.exe,所以python的可执行程序是python.exe,
它把python中的代码翻译成系统函数调用执行,从某种程度上讲python源码是python.exe(C/C++程序)一个复杂配置文件。
3.4. 一个编程语言包括什么?
- 对于脚本语言:解释器
- 有解释器就可以执行脚本了
- 对于不能编译成可执行程序的语言:编译器+运行时
- 编译器把源码编译成中间文件
- 运行时(Runtime)执行中间文件
- 对于可以编译成可执行程序的语言: 编译器
- 操作系统就是运行时
4. 一些反思
上学时候应该是大一的时候就学习C语言程序设计,学完之后对C程序如何编译,如何执行却没有具体的认识。
整体的印象是在VC 6.0
中写好程序点一下运行代码就跑起来了,造成一个错觉:使用C就必须有VC6.0,C语言的执行就是写完代码再点击那个神奇的按钮,编译的作用是体现不出来的。后来学习Java就没有这种感觉,因为老师教学时用了一个文本编辑器+javac命令编译。
然后我们就知道了写java代码有javac和java这俩命令就够了,所以等到后面工作在Linux上运维还是ide开发都还是熟悉的配方,熟悉的命令。
但是C语言就不一样的了,只会用VC6.0,界面又丑,windows又升级还不好安装,基本上不想折腾了。
然后得出一个结论:受限环境有益,当工具不齐全时候更能深刻地认识问题, 就像我们老师说的那样,刚入门学习一个语言不要上来就用ide,会错过很多细节。