编译器编译过程探究

摘要: 完整的语言处理系统包括预处理器、编译器、汇编器、连接-编辑器四个组成部分。一个典型的编译过程为:预处理器将源程序中的宏展开为原始语句加入到源程序中,编译器则产生汇编代码,汇编代码交由汇编器产生可重定位机器代码,然后与一些库程序连接在一起形成绝对机器代码,即可在计算机上执行的代码。本文以GCC为工具,对简单的C,C++程序进行编译,观察其各个部分的输出内容,探究语言处理系统所做的完整工作。

关键字:预处理 编译 汇编 链接 问题探究

1 引言

        一个 C 或者 C++ 语言程序在编译成为可执行目标程序的过程中需要经历预处理、编译、汇编、链接四个阶段。如下图所示:

源程序
目标汇编程序
可重定位机器代码
绝对机器代码
预处理器
编译器
汇编器
连接器
可执行目标程序

本文将采用如下几段 C 或 C++ 代码,对编译的过程及结果进行分析,探究语言系统所做的完整工作。

// test.cc
#include<iostream>
using namespace std;
int main(){
	int i,n,f;
	cin>>n;
	i=2;
	f=1;
	while(i<=n){
		f=f*i;
		i=i+1;
	}
	cout<<f<<endl;
}

//test2.c
#include <stdio.h>
#include "mymath.h"// 自定义头文件
int main(){
    int a = 2;
    int b = 3;
    int sum = add(a, b); 
    printf("a=%d, b=%d, a+b=%d\n", a, b, sum);
}
// mymath.h(储存在文件夹 math 中)
#ifndef MYMATH_H
#define MYMATH_H
int add(int a, int b){return a+b;}
int sub(int a, int b){return a-b;}
#endif

2 预处理阶段

        预处理器产生编译器的输入。通过预处理,可以将储存在不同文件中的程序模块集成为一个完成的源程序,另外还可以将宏展开为原始语句加入到头文件中。其功能概括为:宏处理、文件包含、语言扩充、'理性’预处理器。
在命令行中执行语句:
Gcc -E test.cc -o test.iiGcc -E -I./math test2.c -o test2.i
此处,-E 要求 Gcc 只进行预处理而不进行后面的三个阶段,-I 指出头文件所在的目录, -o 指目标文件,.i 文件为预处理后的 C 源程序。预处理后的代码如下:

//test.ii  (由于原文件过长,此处截取了部分代码)
# 1 "test.cc"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "test.cc"
namespace std
{
  typedef unsigned int size_t;
  typedef int ptrdiff_t;


  typedef decltype(nullptr) nullptr_t;

}
namespace std
{
  inline namespace __cxx11 __attribute__((__abi_tag__ ("cxx11"))) { }
}
namespace __gnu_cxx
{
  inline namespace __cxx11 __attribute__((__abi_tag__ ("cxx11"))) { }
}
namespace std
{


extern "C" {

 __attribute__((__cdecl__)) __attribute__((__nothrow__)) int iswalnum (wint_t);
 __attribute__((__cdecl__)) __attribute__((__nothrow__)) int iswalpha (wint_t);
 __attribute__((__cdecl__)) __attribute__((__nothrow__)) int iswascii (wint_t);
 __attribute__((__cdecl__)) __attribute__((__nothrow__)) int iswcntrl (wint_t);
 __attribute__((__cdecl__)) __attribute__((__nothrow__)) int iswctype (wint_t, wctype_t);
 __attribute__((__cdecl__)) __attribute__((__nothrow__)) int iswdigit (wint_t);
 __attribute__((__cdecl__)) __attribute__((__nothrow__)) int iswgraph (wint_t);
 __attribute__((__cdecl__)) __attribute__((__nothrow__)) int iswlower (wint_t);
 __attribute__((__cdecl__)) __attribute__((__nothrow__)) int iswprint (wint_t);
 __attribute__((__cdecl__)) __attribute__((__nothrow__)) int iswpunct (wint_t);
 __attribute__((__cdecl__)) __attribute__((__nothrow__)) int iswspace (wint_t);
 __attribute__((__cdecl__)) __attribute__((__nothrow__)) int iswupper (wint_t);
 __attribute__((__cdecl__)) __attribute__((__nothrow__)) int iswxdigit (wint_t);

__attribute__((__deprecated__))



 __attribute__((__cdecl__)) __attribute__((__nothrow__)) int is_wctype (wint_t, wctype_t);


__attribute__((__cdecl__)) __attribute__((__nothrow__)) int iswblank (wint_t);



 __attribute__((__cdecl__)) __attribute__((__nothrow__)) wint_t towlower (wint_t);
 __attribute__((__cdecl__)) __attribute__((__nothrow__)) wint_t towupper (wint_t);
 
 ...
 
 # 2 "test.cc"
using namespace std;
int main(){
 int i,n,f;
 cin>>n;
 i=2;
 f=1;
 while(i<=n){
  f=f*i;
  i=i+1;
 }
 cout<<f<<endl;
}

//test2.i (由于原文件过长,此处截取了部分代码)
# 1 "test4.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "test4.c"
typedef struct _iobuf
{
  char *_ptr;
  int _cnt;
  char *_base;
  int _flag;
  int _file;
  int _charbuf;
  int _bufsiz;
  char *_tmpfname;
} FILE;

...

# 2 "test4.c" 2
# 1 "./math/mymath.h" 1

# 4 "./math/mymath.h"
int add(int a, int b){return a+b;}
int sub(int a, int b){return a-b;}
# 3 "test4.c" 2
int main(){
    int a = 2;
    int b = 3;
    int sum = add(a, b);
    printf("a=%d, b=%d, a+b=%d\n", a, b, sum);
}

可以看出头文件及宏定义的代码已经加入到源程序中,所以 .i 文件的体积远大于原文件的体积。

3 编译阶段

        编译器是分阶段执行的,如图为编译器的一个典型的阶段划分:

源程序
词法分析器
语法分析器
语义分析器
中间代码生成器
代码优化器
代码生成器
目标程序

首先是对代码进行语法检查,此处运行命令行代码 gcc -I./math -fsyntax-only test4.c,其中 test4.c 是 test2.c 中 add(a,b) 后去掉分号的文件。 命令行运行后给出如下信息:

test4.c: In function 'main':
test4.c:7:5: error: expected ',' or ';' before 'printf'
     printf("a=%d, b=%d, a+b=%d\n", a, b, sum);
     ^~~~~~

说明出现了语法错误,重新加上 ’ ; ',正常运行。

        另外,在编译阶段还可以对代码进行优化,此处依次运行命令行代码 :gcc -o test2.c,gcc -O3 test2.c,gcc -os test2.c. 其中,-o为默认执行,不进行任何优化;-O3 为优化等次3,-os 为优化代码大小,实际相当于优化等次2.5。将生成.exe文件分别为:a.exe3.exes.exe。不过由于windows下没有直接显示进程运行时间的命令,通过查阅资料,写出如下的批处理,可以输出运行的时间,其代码如下:

//time.bat
@echo off
set /a StartMS=%time:~3,1%*60000 + %time:~4,1%*6000 + %time:~6,1%*1000 + %time:~7,1%*100 + %time:~9,1%*10 + %time:~10,1%
%1 %2 %3 %4 %5 %6
set /a EndMS =%time:~3,1%*60000 + %time:~4,1%*6000 + %time:~6,1%*1000 + %time:~7,1%*100 + %time:~9,1%*10 + %time:~10,1%
set /a realtime = %EndMS%-%StartMS%
echo %realtime%ms

运行命令:.\time.bat D:\Gcc\test2\a.\time.bat D:\Gcc\test2\3.\time.bat D:\Gcc\test2\s即可查看对应的运行时间,运行结果如下:

PS D:\Gcc\test2> .\time.bat D:\Gcc\test2\a
a=2, b=3, a+b=5
6ms
PS D:\Gcc\test2> .\time.bat D:\Gcc\test2\3
a=2, b=3, a+b=5
4ms
PS D:\Gcc\test2> .\time.bat D:\Gcc\test2\s
a=2, b=3, a+b=5
3ms

可以看出,未经优化时,运行消耗的时间更多,-os执行效率最高。

编译阶段的最终结果是生成机器目标代码文件,执行命令:Gcc -S test.cc -o test.SGcc -S test4.c -o test4.S,生成汇编代码如下:

test.S的内容:

    #test.S
	.file	"test.cc"
	.section .rdata,"dr"
__ZStL19piecewise_construct:
	.space 1
.lcomm __ZStL8__ioinit,1,1
	.def	___main;	.scl	2;	.type	32;	.endef
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
LFB1445:
	.cfi_startproc
	leal	4(%esp), %ecx
	.cfi_def_cfa 1, 0
	andl	$-16, %esp
	pushl	-4(%ecx)
	pushl	%ebp
	.cfi_escape 0x10,0x5,0x2,0x75,0
	movl	%esp, %ebp
	pushl	%ecx
	.cfi_escape 0xf,0x3,0x75,0x7c,0x6
	subl	$36, %esp
	call	___main
	leal	-20(%ebp), %eax
	movl	%eax, (%esp)
	movl	$__ZSt3cin, %ecx
	call	__ZNSirsERi
	subl	$4, %esp
	movl	$2, -12(%ebp)
	movl	$1, -16(%ebp)
L3:
	movl	-20(%ebp), %eax
	cmpl	%eax, -12(%ebp)
	jg	L2
	movl	-16(%ebp), %eax
	imull	-12(%ebp), %eax
	movl	%eax, -16(%ebp)
	addl	$1, -12(%ebp)
	jmp	L3
L2:
	movl	-16(%ebp), %eax
	movl	%eax, (%esp)
	movl	$__ZSt4cout, %ecx
	call	__ZNSolsEi
	subl	$4, %esp
	movl	$__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, (%esp)
	movl	%eax, %ecx
	call	__ZNSolsEPFRSoS_E
	subl	$4, %esp
	movl	$0, %eax
	movl	-4(%ebp), %ecx
	.cfi_def_cfa 1, 0
	leave
	.cfi_restore 5
	leal	-4(%ecx), %esp
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
LFE1445:
	.def	___tcf_0;	.scl	3;	.type	32;	.endef
___tcf_0:
LFB1875:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	subl	$8, %esp
	movl	$__ZStL8__ioinit, %ecx
	call	__ZNSt8ios_base4InitD1Ev
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
LFE1875:
	.def	__Z41__static_initialization_and_destruction_0ii;	.scl	3;	.type	32;	.endef
__Z41__static_initialization_and_destruction_0ii:
LFB1874:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	subl	$24, %esp
	cmpl	$1, 8(%ebp)
	jne	L8
	cmpl	$65535, 12(%ebp)
	jne	L8
	movl	$__ZStL8__ioinit, %ecx
	call	__ZNSt8ios_base4InitC1Ev
	movl	$___tcf_0, (%esp)
	call	_atexit
L8:
	nop
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
LFE1874:
	.def	__GLOBAL__sub_I_main;	.scl	3;	.type	32;	.endef
__GLOBAL__sub_I_main:
LFB1876:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	subl	$24, %esp
	movl	$65535, 4(%esp)
	movl	$1, (%esp)
	call	__Z41__static_initialization_and_destruction_0ii
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
LFE1876:
	.section	.ctors,"w"
	.align 4
	.long	__GLOBAL__sub_I_main
	.ident	"GCC: (MinGW.org GCC-6.3.0-1) 6.3.0"
	.def	__ZNSirsERi;	.scl	2;	.type	32;	.endef
	.def	__ZNSolsEi;	.scl	2;	.type	32;	.endef
	.def	__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_;	.scl	2;	.type	32;	.endef
	.def	__ZNSolsEPFRSoS_E;	.scl	2;	.type	32;	.endef
	.def	__ZNSt8ios_base4InitD1Ev;	.scl	2;	.type	32;	.endef
	.def	__ZNSt8ios_base4InitC1Ev;	.scl	2;	.type	32;	.endef
	.def	_atexit;	.scl	2;	.type	32;	.endef

以下为test4.S的内容:

    //test4.S
	.file	"test4.c"
	.text
	.globl	_add
	.def	_add;	.scl	2;	.type	32;	.endef
_add:
LFB10:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	movl	8(%ebp), %edx
	movl	12(%ebp), %eax
	addl	%edx, %eax
	popl	%ebp
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
LFE10:
	.globl	_sub
	.def	_sub;	.scl	2;	.type	32;	.endef
_sub:
LFB11:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	movl	8(%ebp), %eax
	subl	12(%ebp), %eax
	popl	%ebp
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
LFE11:
	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
LC0:
	.ascii "a=%d, b=%d, a+b=%d\12\0"
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
LFB12:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	andl	$-16, %esp
	subl	$32, %esp
	call	___main
	movl	$2, 28(%esp)
	movl	$3, 24(%esp)
	movl	24(%esp), %eax
	movl	%eax, 4(%esp)
	movl	28(%esp), %eax
	movl	%eax, (%esp)
	call	_add
	movl	%eax, 20(%esp)
	movl	20(%esp), %eax
	movl	%eax, 12(%esp)
	movl	24(%esp), %eax
	movl	%eax, 8(%esp)
	movl	28(%esp), %eax
	movl	%eax, 4(%esp)
	movl	$LC0, (%esp)
	call	_printf
	movl	$0, %eax
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
LFE12:
	.ident	"GCC: (MinGW.org GCC-6.3.0-1) 6.3.0"
	.def	_printf;	.scl	2;	.type	32;	.endef

编译器将高级语言源程序转换为汇编语言程序,体现在文件上为将.i文件转换为.S文件。

4 汇编阶段

        通过编译器产生的汇编代码需要交由汇编器进行进一步的处理,生成可重定位的机器代码,体现在文件上为将.S文件转换为.o文件。此时生成的.o文件为二进制文件,文本编辑器将无法打开。为查看生成文件的内容,此处采用GUN的objdump进行反汇编。具体过程为:

Gcc test.S -o test.o //执行完汇编阶段即停止
objdump -d test.o //对文件 test.o 进行反汇编

执行结果如下:

//反汇编后的代码:

test.o:     file format pe-i386

Disassembly of section .text:

00000000 <_main>:
   0:   8d 4c 24 04             lea    0x4(%esp),%ecx
   4:   83 e4 f0                and    $0xfffffff0,%esp
   7:   ff 71 fc                pushl  -0x4(%ecx)
   a:   55                      push   %ebp
   b:   89 e5                   mov    %esp,%ebp
   d:   51                      push   %ecx
   e:   83 ec 24                sub    $0x24,%esp
  11:   e8 00 00 00 00          call   16 <_main+0x16>
  16:   8d 45 ec                lea    -0x14(%ebp),%eax
  19:   89 04 24                mov    %eax,(%esp)
  1c:   b9 00 00 00 00          mov    $0x0,%ecx
  21:   e8 00 00 00 00          call   26 <_main+0x26>
  26:   83 ec 04                sub    $0x4,%esp
  29:   c7 45 f4 02 00 00 00    movl   $0x2,-0xc(%ebp)
  30:   c7 45 f0 01 00 00 00    movl   $0x1,-0x10(%ebp)
  37:   8b 45 ec                mov    -0x14(%ebp),%eax
  3a:   39 45 f4                cmp    %eax,-0xc(%ebp)
  3d:   7f 10                   jg     4f <_main+0x4f>
  3f:   8b 45 f0                mov    -0x10(%ebp),%eax
  42:   0f af 45 f4             imul   -0xc(%ebp),%eax
  46:   89 45 f0                mov    %eax,-0x10(%ebp)
  49:   83 45 f4 01             addl   $0x1,-0xc(%ebp)
  4d:   eb e8                   jmp    37 <_main+0x37>
  4f:   8b 45 f0                mov    -0x10(%ebp),%eax
  52:   89 04 24                mov    %eax,(%esp)
  55:   b9 00 00 00 00          mov    $0x0,%ecx
  5a:   e8 00 00 00 00          call   5f <_main+0x5f>
  5f:   83 ec 04                sub    $0x4,%esp
  62:   c7 04 24 00 00 00 00    movl   $0x0,(%esp)
  69:   89 c1                   mov    %eax,%ecx
  6b:   e8 00 00 00 00          call   70 <_main+0x70>
  70:   83 ec 04                sub    $0x4,%esp
  73:   b8 00 00 00 00          mov    $0x0,%eax
  78:   8b 4d fc                mov    -0x4(%ebp),%ecx
  7b:   c9                      leave
  7c:   8d 61 fc                lea    -0x4(%ecx),%esp
  7f:   c3                      ret

00000080 <___tcf_0>:
  80:   55                      push   %ebp
  81:   89 e5                   mov    %esp,%ebp
  83:   83 ec 08                sub    $0x8,%esp
  86:   b9 00 00 00 00          mov    $0x0,%ecx
  8b:   e8 00 00 00 00          call   90 <___tcf_0+0x10>
  90:   c9                      leave
  91:   c3                      ret

00000092 <__Z41__static_initialization_and_destruction_0ii>:
  92:   55                      push   %ebp
  93:   89 e5                   mov    %esp,%ebp
  95:   83 ec 18                sub    $0x18,%esp
  98:   83 7d 08 01             cmpl   $0x1,0x8(%ebp)
  9c:   75 1f                   jne    bd <__Z41__static_initialization_and_destruction_0ii+0x2b>
  9e:   81 7d 0c ff ff 00 00    cmpl   $0xffff,0xc(%ebp)
  a5:   75 16                   jne    bd <__Z41__static_initialization_and_destruction_0ii+0x2b>
  a7:   b9 00 00 00 00          mov    $0x0,%ecx
  ac:   e8 00 00 00 00          call   b1 <__Z41__static_initialization_and_destruction_0ii+0x1f>
  b1:   c7 04 24 80 00 00 00    movl   $0x80,(%esp)
  b8:   e8 00 00 00 00          call   bd <__Z41__static_initialization_and_destruction_0ii+0x2b>
  bd:   90                      nop
  be:   c9                      leave
  bf:   c3                      ret

000000c0 <__GLOBAL__sub_I_main>:
  c0:   55                      push   %ebp
  c1:   89 e5                   mov    %esp,%ebp
  c3:   83 ec 18                sub    $0x18,%esp
  c6:   c7 44 24 04 ff ff 00    movl   $0xffff,0x4(%esp)
  cd:   00
  ce:   c7 04 24 01 00 00 00    movl   $0x1,(%esp)
  d5:   e8 b8 ff ff ff          call   92 <__Z41__static_initialization_and_destruction_0ii>
  da:   c9                      leave
  db:   c3                      ret

5 装载-连接阶段

        装配器完成程序的装入和连接编辑两项功能。装入过程包括读入可重定位机器代码,修改可重定位地址,并将修改后的指令和数据放到内存中的合适位置。连接编辑器将多个重定位机器代码的文件组装成为一个程序。

        经过汇编之后的.o文件依然是不可执行的,只有经过连接阶段,将程序所引用的外部文件关联起来,生成.exe文件才是可执行的程序。

        连接的方式有静态链接与动态链接,动态链接的代码是存放在动态链接库或者某个共享对象的目标文件中,不会将库的内容拷贝到可执行程序中,所以生成的程序的体积较小;而静态链接库则将需要的代码从相应的静态链接库中拷贝到可执行程序中。此处采用了动态链接库的方式。命令代码为:

gcc -fPIC -shared test.cc -o libtest.so  //链接库libtest.so
gcc test.o -o test  //生成可执行程序test.exe
.\test  //执行test.exe  

运行结果如下:

D:\Gcc\test>.\test
5
120

libtest.so 反汇编后的代码(由于文件过长截取部分内容,详见附件):

D:\Gcc\test>objdump -d libtest.so

libtest.so:     file format pei-i386


Disassembly of section .text:

61d41000 <.text>:
61d41000:       53                      push   %ebx
61d41001:       83 ec 18                sub    $0x18,%esp
61d41004:       8b 15 04 50 d4 61       mov    0x61d45004,%edx
61d4100a:       85 d2                   test   %edx,%edx
61d4100c:       74 39                   je     61d41047 <.text+0x47>

        .......

61d41ba0 <__DTOR_LIST__>:
61d41ba0:       ff                      (bad)
61d41ba1:       ff                      (bad)
61d41ba2:       ff                      (bad)
61d41ba3:       ff 00                   incl   (%eax)
61d41ba5:       00 00                   add    %al,(%eax)

        

参考

  1. 肖文鹏. Linux汇编语言开发指南(EB/OL). http://www.ibm.com/developerworks/cn/linux/l-assembly/index.html, 2003-07-03
  2. 作者不详. GCC优化选项的各种含义以及潜藏风险(EB/OL). http://www.360doc.com/content/16/0802/17/478627_580294703.shtml, 2016-08-02
  3. 作者不详. bat时间的运算与提取(EB/OL). https://www.cnblogs.com/--3q/p/5723277.html, 2016-07-31
  4. gcc manual(EB/OL). http://gcc.gnu.org/onlinedocs/gcc-4.2.2/gcc/
  5. gcc中文文档
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值