摘要: 完整的语言处理系统包括预处理器、编译器、汇编器、连接-编辑器四个组成部分。一个典型的编译过程为:预处理器将源程序中的宏展开为原始语句加入到源程序中,编译器则产生汇编代码,汇编代码交由汇编器产生可重定位机器代码,然后与一些库程序连接在一起形成绝对机器代码,即可在计算机上执行的代码。本文以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.ii
和 Gcc -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.exe
、3.exe
、s.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.S
、Gcc -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)
参考
- 肖文鹏. Linux汇编语言开发指南(EB/OL). http://www.ibm.com/developerworks/cn/linux/l-assembly/index.html, 2003-07-03
- 作者不详. GCC优化选项的各种含义以及潜藏风险(EB/OL). http://www.360doc.com/content/16/0802/17/478627_580294703.shtml, 2016-08-02
- 作者不详. bat时间的运算与提取(EB/OL). https://www.cnblogs.com/--3q/p/5723277.html, 2016-07-31
- gcc manual(EB/OL). http://gcc.gnu.org/onlinedocs/gcc-4.2.2/gcc/
- gcc中文文档