C语言变成可执行文件的四大步骤

从C语言变成最终的可执行文件,需要经过四步:

预处理;

编译;

汇编;

链接。

下面就以Linux环境为例,来分析下四个步骤。

预处理

写一段简单的代码:

#include

#define OK 0

int main()

{

printf(“hellowrld

“);

return OK;

}

其中,井号键开头的代码有两行:包含头文件和宏定义。 预处理命令:

root@Turbo:t# gcc -E test.c -o test.i

root@Turbo:t# ls

test test.c test.i

root@Turbo:t#

预处理后的test.i代码:

# 1 “test.c”

# 1 “”

# 1 “”

# 31 “”

# 1 “/usr/include/stdc-predef.h” 1 3 4

# 32 “” 2

# 1 “test.c”

# 1 “/usr/include/stdio.h” 1 3 4

# 27 “/usr/include/stdio.h” 3 4

# 1 “/usr/include/x86_64-linux-gnu/bits/libc-header-start.h” 1 3 4

# 33 “/usr/include/x86_64-linux-gnu/bits/libc-header-start.h” 3 4

# 1 “/usr/include/features.h” 1 3 4

# 461 “/usr/include/features.h” 3 4

# 1 “/usr/include/x86_64-linux-gnu/sys/cdefs.h” 1 3 4

# 452 “/usr/include/x86_64-linux-gnu/sys/cdefs.h” 3 4

# 1 “/usr/include/x86_64-linux-gnu/bits/wordsize.h” 1 3 4

# 453 “/usr/include/x86_64-linux-gnu/sys/cdefs.h” 2 3 4

# 1 “/usr/include/x86_64-linux-gnu/bits/long-double.h” 1 3 4

# 454 “/usr/include/x86_64-linux-gnu/sys/cdefs.h” 2 3 4

# 462 “/usr/include/features.h” 2 3 4

……………

# 5 “test.c”

int main()

{

printf(“hellowrld

“);

return 0;

}

代码量从原来的10行变成了七百多行,主要的变化有两个:

头文件没了;

return OK 变成了 return 0。

所以基本上可以得出预处理的作用: 处理所有以井号键开头的代码,包括头文件、宏定义、条件编译等等。

头文件展开。以stdio.h为例,编译器会去默认的目录下(一般是/usr/include)找到这个文件,然后把里面的内容复制一份,粘贴到C文件中。这就是为什么预处理后的文件变成了七百多行。

宏定义替换。预处理的时候如果遇到了宏定义,直接把宏替换掉,比如代码中的OK就变成了数字0。

条件编译。下面的代码就属于条件编译:

#ifndef _STDIO_H

#define _STDIO_H

#endif

条件编译会在预处理的时候做出判断,满足条件的代码留下来,不满足的去掉。

编译

编译的命令:

root@Turbo:t# gcc -S test.i -o test.s

root@Turbo:t# ls

test test.c test.i test.s

root@Turbo:t#

查看编译后的文件test.s内容:

.file “test.c”

.text

.section .rodata

.LC0:

.string “hellowrld”

.text

.globl main

.type main, @function

main:

.LFB0:

.cfi_startproc

endbr64

pushq %rbp

.cfi_def_cfa_offset 16

.cfi_offset 6, -16

movq %rsp, %rbp

.cfi_def_cfa_register 6

leaq .LC0(%rip), %rdi

call puts@PLT

movl $0, %eax

popq %rbp

.cfi_def_cfa 7, 8

ret

.cfi_endproc

.LFE0:

.size main, .-main

.ident “GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0”

.section .note.GNU-stack,””,@progbits

.section .note.gnu.property,”a”

.align 8

.long 1f – 0f

.long 4f – 1f

.long 5

0:

.string “GNU”

1:

.align 8

.long 0xc0000002

.long 3f – 2f

2:

.long 0x3

3:

.align 8

4:

如果你搞过单片机的话,一定能看出来这是汇编代码。

编译的作用:

语法检查;

把C代码翻译成汇编代码。

汇编 汇编的命令:

root@Turbo:t# gcc -c test.s -o test.o

root@Turbo:t# ls

test test.c test.i test.o test.s

查看汇编后test.o的内容:

很显然, 这是一个二进制文件。

既然是二进制文件,那能不能执行呢?答案是不行,原因也很简单,因为此时代码还不知道printf函数在哪。

汇编的作用:

把汇编代码翻译成二进制代码。

链接

链接的命令:

root@Turbo:t# gcc test.o -o test

root@Turbo:t# ls

test test.c test.i test.o test.s

root@Turbo:t# ./test

hellowrld

如果工程里面有多个C文件,就会产生多个.o文件,链接的时候,把所有.o文件加上就行。

链接的作用:

把所有需要的源文件合并到一起;

链接代码中需要用到的库(比如printf,需要链接C库)。

审核编辑:汤梓红

发表评论