从C语言编译看高级程序语言执行

从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,会错过很多细节。

原文地址:https://www.cnblogs.com/oaks/p/13113683.html