C语言编译过程详解

C语言程序从源代码到二进制行程序都经历了那些过程?本文以Linux下C语言的编译过程为例,理解C语言程序的编译过程。

单文件编译过程

编写hello worldC程序:

// main.c
#include <stdio.h>
int main()
{
    printf("hello world\n");
    return 0;
}

编译+执行:

$ gcc hello.c # 编译
$ ./a.out # 执行
hello world!

上述gcc命令其实依次执行了四步操作:

  1. 预处理(Preprocessing)
  2. 编译(Compilation)
  3. 汇编(Assemble)
  4. 链接(Linking)。
《C语言编译过程详解》

多文件编译过程

本示例中,一共有3个文件,文件结构如下:

├── main.c
└── src
    ├── mymath.c
    └── mymath.h

3个文件内容分别为:

// main.c
#include <stdio.h>
#include "mymath.h"
int main()
{
    int a = 10, b = 5;
    printf("%d, %d", add(a, b), sub(a, b));
    return 0;
}
// mymath.h
#ifndef __MYMATH_H
#define __MYMATH_H

int add(int a, int b);
int sub(int a, int b);

#endif
// mymath.c
#include "mymath.h"

int add(int a, int b)
{
    return a + b;
}
int sub(int a, int b)
{
    return a - b;
}

1、预处理(Preprocessing)

预处理用于将所有的#include头文件以及宏定义替换成其真正的内容,预处理之后得到的仍然是文本文件,但文件体积会大很多。gcc的预处理是预处理器cpp来完成的,通过如下命令对main.c进行预处理:

gcc -E -I./src main.c -o main.i

或者直接调用cpp命令

cpp main.c -I./src -o main.i

上述命令中-E是让编译器在预处理之后就退出,不进行后续编译过程;-I指定头文件目录,这里指定的是我们自定义的头文件目录;-o指定输出文件名。

经过预处理之后代码体积会大很多,使用ls -l查看文件大小:

-rwxrwxrwx 1 hamlin hamlin   145 2月  17 11:16 main.c
-rwxrwxrwx 1 hamlin hamlin 17238 2月  17 11:43 main.i

预处理之后的程序还是文本,可以用文本编辑器打开

2、编译(Compilation)

这里的编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程。编译的指定如下:

gcc -S -I./src main.c -o main.s
上述命令中-S让编译器在编译之后停止,不进行后续过程。编译过程完成后,将生成程序的汇编代码main.s,这也是文本文件,内容如下:

    .file   "main.c"
    .section    .rodata
.LC0:
    .string "%d, %d"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    pushq   %rbx
    subq    $24, %rsp
    .cfi_offset 3, -24
    movl    $10, -24(%rbp)
    movl    $5, -20(%rbp)
    movl    -20(%rbp), %edx
    movl    -24(%rbp), %eax
    movl    %edx, %esi
    movl    %eax, %edi
    call    sub
    movl    %eax, %ebx
    movl    -20(%rbp), %edx
    movl    -24(%rbp), %eax
    movl    %edx, %esi
    movl    %eax, %edi
    call    add
    movl    %ebx, %edx
    movl    %eax, %esi
    movl    $.LC0, %edi
    movl    $0, %eax
    call    printf
    movl    $0, %eax
    addq    $24, %rsp
    popq    %rbx
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
    .section    .note.GNU-stack,"",@progbits

3、汇编(Assemble)

汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式。
gcc汇编过程通过as命令完成:

as main.s -o main.o

等价于:

gcc -c main.s -o main.o

这一步会为每一个源文件产生一个目标文件。因此mymath.c也需要产生一个mymath.o文件:

gcc -E ./src/mymath.c -o ./src/mymath.i
gcc -S ./src/mymath.c -o ./src/mymath.s
gcc -c ./src/mymath.s -o ./src/mymath.o

4、链接(Linking)

链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)。

命令大致如下:

ld -o main.out main.o src/mymath.o ...libraries...

根据此命令进行链接会发生以下报错:

ld -o main.out main.o ./src/mymath.o
ld: 警告: 无法找到项目符号 _start; 缺省为 00000000004000b0
main.o:在函数‘main’中:
main.c:(.text+0x46):对‘printf’未定义的引用

原因:c程序以main为主函数,汇编以start为主函数入口

故直接使用gcc -o main.out main.o ./src/mymath.o命令生成main.out可执行文件


进行到此步骤后,文件结构如下:

├── main.c
├── main.i
├── main.o
├── main.out
├── main.s
└── src
    ├── mymath.c
    ├── mymath.h
    ├── mymath.i
    ├── mymath.o
    └── mymath.s

参考资料

1、C语言编译过程详解

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注