Linux 程序编译过程

前言

计算机程序设计语言通常分为机器语言,汇编语言和高级语言三类。而高级语言需要被翻译成机器语言才可以被执行,而翻译的方式也被分为两种,一种是编译型,另一种为解释型,根据这两种的不同,我们将其分为编译型语言(例如c,c++,java等)与解释型语言(例如python,javascript等)

那么什么是机器语言,什么是汇编语言,什么是高级语言喃?
机器语言: 也称为低级语言,二进制语言,是一种指令集的体系。这种指令集,称机器码(machine code),是电脑的CPU可直接解读的数据
汇编语言: 是一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言
高级语言: 是相对于汇编语言而言的,它是较接近自然语言和数学公式的编程,基本脱离了机器的硬件系统,用人们更易理解的方式编写程序

那么什么又是解释型语言与编译型语言喃?
解释型语言: 是指使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行的语言。解释型语言通常不会进行整体性的编译和链接处理,解释型语言相当于把编译型语言中的编译和解释过程混合到一起同时完成。
编译型语言: 是指使用专门的编译器,针对特定平台将某种高级语言源代码一次性“翻译”成可被该平台硬件执行的机器码,并包装成该平台所能识别的可执行性程序的格式,这个转换过程称为编译(Compile)。编译生成的可执行性程序可以脱离开发环境,在特定的平台上独立运行。

程序编译过程

程序编译过程分为四个阶段,分别为预处理,编译,汇编,链接这几个阶段
在这里插入图片描述

下面我们分别介绍这几个阶段中,具体会进行什么样的操作:

预处理: 这个阶段会进行条件编译宏替换头文件包含等操作,经过预处理器将.c文件处理生成一个后缀为.i的文件
hello.c 源码如下

#include <stdio.h>
#define N 1
int main()
{
        int a;
#ifdef AAA
        a = N
#else
        a = 10
#endif
        printf("a= %d\n", a);
        printf("hello, world");
        return 0;
}

经过如下指令处理后,hello.i会进行宏定义展开,头文件替换

gcc -E hello.c -o hello.i

经过预处理后的hello.i文件内容如下

...
extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__));
# 840 "/usr/include/stdio.h" 3 4
extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));



extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;


extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 858 "/usr/include/stdio.h" 3 4
extern int __uflow (FILE *);
extern int __overflow (FILE *, int);
# 873 "/usr/include/stdio.h" 3 4

# 2 "hello.c" 2


# 3 "hello.c"
int main()
{
 int a;



 a = 10

 printf("a= %d\n", a);
 printf("hello, world");
        return 0;
}

定义AAA宏再次重新进行预处理,指令如下

gcc -E hello.c -o hello.i -DAAA

再次查看你会发现a的值随着AAA宏定义而变化

extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__));
# 840 "/usr/include/stdio.h" 3 4
extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));



extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;


extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 858 "/usr/include/stdio.h" 3 4
extern int __uflow (FILE *);
extern int __overflow (FILE *, int);
# 873 "/usr/include/stdio.h" 3 4

# 2 "hello.c" 2


# 3 "hello.c"
int main()
{
 int a;

 a = 1



 printf("a= %d\n", a);
 printf("hello, world");
        return 0;
}

编译: 此阶段会进行词法分析语法分析语义分析中间代码产生代码优化阶段目标代码生成等几个过程,它会从hello.i文件生成hello.s文件,生成汇编代码
指令如下

gcc -S hello.i -o hello.s

举个例子: 我们将源码中的int改为in,在这个阶段中进行语词分析的时候就会抛出错误

hello.c: In function ‘main’:
hello.c:5:2: error: unknown type name ‘in’; did you mean ‘int?
    5 |  in a = N;
      |  ^~
      |  int

恢复int查看hello.s文件

        .file   "hello.c"
        .text
        .section        .rodata
.LC0:
        .string "a= %d\n"
.LC1:
        .string "hello, world"
        .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
        subq    $16, %rsp
        movl    $1, -4(%rbp)
        movl    -4(%rbp), %eax
        movl    %eax, %esi
        leaq    .LC0(%rip), %rdi
        movl    $0, %eax
        call    printf@PLT
        leaq    .LC1(%rip), %rdi
        movl    $0, %eax
        call    printf@PLT
        movl    $0, %eax
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.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:

汇编: 将汇编代码翻译成二进制代码(.s文件生成.o文件)
编译指令如下

gcc -c hello.s -o hello.o

直接文本形式打开hello.o 是乱码,需反汇编工具才可以帮助我们读懂它
例如使用如下指令将其反汇编

objdump -S hello.o

反汇编之后我们可以看到如下信息

hello.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:	f3 0f 1e fa          	endbr64 
   4:	55                   	push   %rbp
   5:	48 89 e5             	mov    %rsp,%rbp
   8:	48 83 ec 10          	sub    $0x10,%rsp
   c:	c7 45 fc 01 00 00 00 	movl   $0x1,-0x4(%rbp)
  13:	8b 45 fc             	mov    -0x4(%rbp),%eax
  16:	89 c6                	mov    %eax,%esi
  18:	48 8d 3d 00 00 00 00 	lea    0x0(%rip),%rdi        # 1f <main+0x1f>
  1f:	b8 00 00 00 00       	mov    $0x0,%eax
  24:	e8 00 00 00 00       	callq  29 <main+0x29>
  29:	48 8d 3d 00 00 00 00 	lea    0x0(%rip),%rdi        # 30 <main+0x30>
  30:	b8 00 00 00 00       	mov    $0x0,%eax
  35:	e8 00 00 00 00       	callq  3a <main+0x3a>
  3a:	b8 00 00 00 00       	mov    $0x0,%eax
  3f:	c9                   	leaveq 
  40:	c3                   	retq 

链接: 此阶段会链接所有的函数、全局变量,将所有的.o文件链接成一个可执行文件(例如hello.c文件调用了printf函数,printf函数存在一个名为printf.o的文件中,而我们必须把printf.o合并到hello.o中

GCC 指令用法

-c:编译生成目标文件(object file),即.o文件
-o :后面接文件名,用来指定gcc生成的文件名,如果没有-o,那么生成的是gcc默认的文件名
-g:产生符号调试工具(gdb)所必要的符号资讯(符号表),要想对源代码进行调试,就必须加入这个选项
-Wall:以最高级别使用GNU编译程序,专门用于显示警告信息
-O/-O1:对程序进行优化编译、链接,这样产生的可执行文件的执行效率可以提高。但是编译。链接的速度可能会变慢
-O2:比-O/-O1更好的优化
-O3:比-O2更好的优化
-O0:指定不优化
-D:后面跟宏定义,在编译时在代码中加上此宏定义
-I(大写的i):后面接程序使用到的头文件的指定路径
-L:后面接程序中使用到的函数库的目录(一般用来指定非系统的函数库)
-l(小写的L):后面接指定函数库的名称,一般与-L成对出现
-v:会显示gcc的版本或gcc具体执行的内容
-E:用来通过.c文件预处理的.i文件
-S:用来将.c/.i文件生成汇编文件(.s)
-fPIC -shared:编译生成动态库文件


版权声明:本文为xiaoxiaohai12原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。