相信很多人同我一样,在刚刚接触C语言的时候,只是找了一本教材,或者是找了一套教学视频,跟着慢慢学习C语言的语法,并没有去多想一个.c文件在后台究竟是经过了怎样的步骤才最终变成.exe文件;就在前几天,本人闲着无聊翻开了在书架上吃灰将近一年的“全新”CSAPP,在看到其第一章的内容之后,恍然大悟,姑且水一篇博客纪念一下。
首先我们来简要看一下CSAPP原书上的内容:(这是我按照自己的理解结合原书内容敲下的,也许会存在事实上的错误,尊重原书的说法。)
一个.c源码最终变成可执行文件要经过以下步骤:
- 预处理阶段:预处理器根据以#开头的指令,修改源码内容,比如如果源码中有 #include
则预处理器会读取文件 stdio.h 文件中的内容,并将其直接插入到原来的源码文件中,通常另存为以 .i 为扩展名的文件。 - 编译阶段:编译器读取 .i 文件中的内容,并将其翻译为以 .s 为扩展名的汇编语言文件。
- 汇编阶段:汇编器将 .s 文件翻译成机器码,并保存为 .o为扩展名的文件。
- 链接阶段:链接器将不同的 .o 文件合并到一起,组成最终的可执行文件;比如我们的程序里调用了 printf 函数,作为一个C标准函数,printf 单独存在于一个 printf.o 的文件中,那么链接器将会找到这个 printf.o 文件,将其中的内容合并到我们自己的 .o 文件中,生成可以被加载到内存中执行的文件。
光看理论没啥意思,让我们来手动操作一下:
首先新建一个 hello.c 文件,在里面敲上经典的C语言hello world代码,保存,进入cmd或powershell,移动到 hello.c 所在的文件夹(linux怎么做肯定不用我讲了,逃ε=ε=ε=┏(゜ロ゜;)┛)。
之后输入如下指令:
gcc -E hello.c -o hello.i
gcc -S hello.c -o hello.s
然后,打开 hello.i 我们就可以看到,预处理器已经对源码进行了修改(文件内容很多,我姑且放一张截图上来)
当然了,在文件的末尾是我们可怜的main函数:
如果想知道完整的文件里有什么,当然是自己动手试一下啦(✿◡‿◡)
然后,我们再打开 hello.s 文件,可以看到原始的 .c 被翻译为 .s 之后的结果:
(CSDN好像没有专门的汇编的渲染,姑且设置成C++)
.file "hello.c"
.text
.def __main; .scl 2; .type 32; .endef
.section .rdata,"dr"
.LC0:
.ascii "hello world\0"
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
pushq %rbp
.seh_pushreg %rbp
movq %rsp, %rbp
.seh_setframe %rbp, 0
subq $32, %rsp
.seh_stackalloc 32
.seh_endprologue
call __main
leaq .LC0(%rip), %rcx
call puts
movl $0, %eax
addq $32, %rsp
popq %rbp
ret
.seh_endproc
.ident "GCC: (x86_64-posix-sjlj-rev0, Built by MinGW-W64 project) 8.1.0"
.def puts; .scl 2; .type 32; .endef
接下来我们还可以用:
gcc hello.c -o hello.o
生成 .o 文件,不过到了这一步。这些东西就完全超出我的姿势范围了。
单个文件玩过了,当然还要玩一下多文件才过瘾,让我们准备三个文件: hell2.c func.c func.h ,内容依次如下:
// hello2.c
#include
#include "func.h"
int main()
{
say_hello();
}
// func.h
#ifndef FUNC_H
#define FUNC_H
#include
void say_hello();
#endif
// func.c
#include "func.h"
void say_hello()
{
printf("hello\n");
}
之后还是进入命令行模式,依次输入以下指令:
gcc -c func.c -o func.o
gcc -E hello2.c -o hello2.i
gcc -S hello2.c -o hello2.s
gcc hello2.c func.o -o hello2.exe
如果没有错误,我们将得到 func.c 编译生成的 .o 文件, hello2.c 经过预处理器和编译器处理之后得到的 .i 和 .s 文件,以及 hello2.c 最终生成的可执行文件。
打开 hello2.i ,我们将看到 #include "func.h" 被预处理器替换之后的情况:
// 上面省略100多行天书
# 1 "C:/mingw64/x86_64-w64-mingw32/include/_mingw_print_pop.h" 1 3
# 1400 "C:/mingw64/x86_64-w64-mingw32/include/stdio.h" 2 3
# 3 "hello2.c" 2
# 1 "func.h" 1
# 6 "func.h"
void say_hello();
# 4 "hello2.c" 2
int main()
{
say_hello();
}
打开 hello2.s ,我们可以看到“精简”的汇编代码:
.file "hello2.c"
.text
.def __main; .scl 2; .type 32; .endef
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
pushq %rbp
.seh_pushreg %rbp
movq %rsp, %rbp
.seh_setframe %rbp, 0
subq $32, %rsp
.seh_stackalloc 32
.seh_endprologue
call __main
call say_hello
movl $0, %eax
addq $32, %rsp
popq %rbp
ret
.seh_endproc
.ident "GCC: (x86_64-posix-sjlj-rev0, Built by MinGW-W64 project) 8.1.0"
.def say_hello; .scl 2; .type 32; .endef
这篇博客到这里就差不多了,水平有限,欢迎指正!
= ̄ω ̄=