简单了解一下c编译过程
大一的时候,学习c语言,用的是VC6.0。用了1年多,到后来了解了Linux,知道了gcc编译器,开始使用gcc Hello.c -o a.out 这样的命令进行编译。后来又学了gcc的一些其他的命令,不同的命令可以编译出不同的目标代码。现在想想类似于VC这种IDE虽然方便,但是对于具体是怎样的一个过程就不得而知了。作为一个优秀的程序员怎么可以不了解这个过程呢。
Gcc/g++ 在执行编译工作的时候,总共4步
1.预处理,生成.i的文件 (预处理器cpp)
2.将预处理后的文件转换成汇编语言,生成文件.s文件 ()
3.从汇编变为目标代码(机器代码)生成.o(.obj)的文件 (汇编器as)
4.连接目标代码,生成可执行程序 (连接器ld)
我们现在先写一个Hello World吧
1 #include <stdio.h>
2 #define WOELD "World"
3 static int a=0;
4 int main()
5 {
6 int j,jj;
7 int i=0;
8 char ch='a';
9 printf("Hello ");
10 printf(WOELD);
11 printf("\n%d %d %c\n",i,a,ch);
12 return 0;
13 }
先预处理一下,可以使用cpp命令,或者是使用gcc 的-E选项
gcc -E Hello.c > Hello.i
一个简单的Hello World都要800多行。
可以看到这些都是函数的声明。而函数的定义是在链接库中。我们可以认为是写了好多函数了。然后在main中调用。这个跟一般的函数是一个道理的。只是这些函数是又编译器帮你写了。这个就不得不提到,C语言只是定义了标准库函数的输入和输出,至于实现的过程,是没有规定的。这个由编译器厂商自己设计,这就是为什么有人说Linux下gcc编译后的程序会比Windows下VS编译后的程序运行效率上有些区别的一个原因吧。
像上面的printf函数的原型,如果有兴趣可以去下载源代码查看。
下面这个是VC6.0对printf函数的定义
1 int __cdecl printf (
2 const char *format,
3 ...
4 )
5 /*
6 * stdout 'PRINT', 'F'ormatted
7 */
8 {
9 va_list arglist;
10 int buffing;
11 int retval;
12 va_start(arglist, format);
13 _ASSERTE(format != NULL);
14
15 _lock_str2(1, stdout);
16 buffing = _stbuf(stdout);
17 retval = _output(stdout,format,arglist);
18 _ftbuf(buffing, stdout);
19 _unlock_str2(1, stdout);
20 return(retval);
21 }
Gnu也有个对printf函数的定义,下面这个是glibc-2.2.5对printf的定义
1 int
2 printf (const char *format, ...)
3 {
4 va_list arg;
5 int done;
6 va_start (arg, format);
7 done = vfprintf (stdout, format, arg);
8 va_end (arg);
9 return done;
10 }
11 /* The function itself. */
12 int
13 vfprintf (FILE *s, const CHAR_T *format, va_list ap)
14 {
15 /* The character used as thousands separator. */
16 #ifdef COMPILE_WPRINTF
17 wchar_t thousands_sep = L'\0';
18 #else
19 const char *thousands_sep = NULL;
20 #endif
21
22 /* The string describing the size of groups of digits. */
23 const char *grouping;
24 ......
从这里可以看出刚才我们定义的宏 WORLD已经转换为字符串了。所有我们知道所有的预处理或者宏定义都会在这一步完成。
好了,我们现在开始第二步了。生成汇编代码,使用gcc的-S选项。
gcc -S Hello.c -o Hello.s
1 .file "Hello.c"
2 .lcomm _a,4,4
3 .def ___main; .scl 2; .type 32; .endef
4 .section .rdata,"dr"
5 LC0:
6 .ascii "Hello \0"
7 LC1:
8 .ascii "World\0"
9 LC2:
10 .ascii "\12%d %d %c\12\0"
11 .text
12 .globl _main
13 .def _main; .scl 2; .type 32; .endef
14 _main:
15 LFB13:
16 .cfi_startproc
17 pushl %ebp
18 .cfi_def_cfa_offset 8
19 .cfi_offset 5, -8
20 movl %esp, %ebp
21 .cfi_def_cfa_register 5
22 andl $-16, %esp
23 subl $32, %esp
24 call ___main
25 movl $0, 28(%esp)
26 movb $97, 27(%esp)
27 movl $LC0, (%esp)
28 call _printf
29 movl $LC1, (%esp)
30 call _printf
31 movsbl 27(%esp), %edx
32 movl _a, %eax
33 movl %edx, 12(%esp)
34 movl %eax, 8(%esp)
35 movl 28(%esp), %eax
36 movl %eax, 4(%esp)
37 movl $LC2, (%esp)
38 call _printf
39 movl $0, %eax
40 leave
41 .cfi_restore 5
42 .cfi_def_cfa 4, 4
43 ret
44 .cfi_endproc
45 LFE13:
46 .ident "GCC: (rev5, Built by MinGW-W64 project) 4.8.1"
47 .def _printf; .scl 2; .type 32; .endef
总共47行。如果我们使用-O优化
gcc -S -O3 Hello.c -o Hello.s
1 .file "Hello.c"
2 .def ___main; .scl 2; .type 32; .endef
3 .section .rdata,"dr"
4 LC0:
5 .ascii "Hello \0"
6 LC1:
7 .ascii "World\0"
8 LC2:
9 .ascii "\12%d %d %c\12\0"
10 .section .text.startup,"x"
11 .p2align 4,,15
12 .globl _main
13 .def _main; .scl 2; .type 32; .endef
14 _main:
15 LFB13:
16 .cfi_startproc
17 pushl %ebp
18 .cfi_def_cfa_offset 8
19 .cfi_offset 5, -8
20 movl %esp, %ebp
21 .cfi_def_cfa_register 5
22 andl $-16, %esp
23 subl $16, %esp
24 call ___main
25 movl $LC0, (%esp)
26 call _printf
27 movl $LC1, (%esp)
28 call _printf
29 movl $97, 12(%esp)
30 movl $0, 8(%esp)
31 movl $0, 4(%esp)
32 movl $LC2, (%esp)
33 call _printf
34 xorl %eax, %eax
35 leave
36 .cfi_restore 5
37 .cfi_def_cfa 4, 4
38 ret
39 .cfi_endproc
40 LFE13:
41 .ident "GCC: (rev5, Built by MinGW-W64 project) 4.8.1"
42 .def _printf; .scl 2; .type 32; .endef
总共有42行,从这里就可以看出优化参数的作用了。
第三步了。生成目标文件,可以使用gcc的-c参数
gcc -c Hello.c -o Hello.o
生成后的文件有 962字节
gcc -c -O3 Hello.c -o Hello.o
生成后的文件有1022字节
大概就是这样了。这个目标文件的使用,到后面的动态静态连接库的时候提到。
第四步了。
gcc Hello.c -o Hello.exe
gcc Hello.c -o a.out
然后就可以运行了。
动态链接库/静态链接库(补充)
在windows下一般可以看到后缀为dll和后缀为lib的文件而linux下一般是so和lib***.a,但这两种文件可以分为三种库,分别是动态链接库(Dynamic-Link Libraries),目标库(Object Libraries)和导入库(Import Libraries),下面一一解释这三种库。
目标库(Object Libraries)
目标库又叫静态链接库,是扩展名为.LIB(.a)的文件,包括了用户程序要用到的各种函数。它在用户程序进行链接时,“静态链接”到可执行程序文件当中。例如,在VC++中最常使用到的C运行时目标库文件就是LIBC.LIB。在链接应用程序时常使用所谓“静态链接”的方法,即将各个目标文件(.obj)、运行时函数库(.lib)以及已编译的资源文件(.res)链接到一起,形成一个可执行文件(.exe)。使用静态链接时,可执行文件需要使用的各种函数和资源都已包含到文件中。这样做的缺点是对于多个程序都使用的相同函数和资源要重复链接到exe文件中,使程序变大、占用内存增加。
导入库(Import Libraries)
导入库是一种特殊形式的目标库文件形式。和目标库文件一样,导入库文件的扩展名也是.LIB(.so),也是在用户程序被链接时,被“静态链接”到可执行文件当中。但是不同的是,导入库文件中并不包含有程序代码。相应的,它包含了相关的链接信息,帮助应用程序在可执行文件中建立起正确的对应于动态链接库的重定向表。比如KERNEL32.LIB、USER32.LIB和GDI32.LIB就是我们常用到的导入库,通过它们,我们就可以调用Windows提供的函数了。如果我们在程序中使用到了Rectangle这个函数,GDI32.LIB就可以告诉链接器,这个函数在GDI32.DLL动态链接库文件中。这样,当用户程序运行时,它就知道“动态链接”到GDI32.DLL模块中以使用这个函数。
动态链接库(Dynamic-Link Libraries)
“动态链接”是将一些公用的函数或资源组织成动态链接库文件(.dll),当某个需要使用dll中的函数或资源的程序启动时(准确的说是初始化时),系统将该 dll映射到调用进程的虚拟地址空间、增加该dll的引用计数值,然后当实际使用到该dll时操作系统就将该dll载入内存;如果使用该dll的所有程序都已结束,则系统将该库从内存中移除。使用同一dll的各个进程在运行时共享dll的代码,但是对于dll中的数据则各有一份拷贝(当然也有在dll中共享数据的方法)。动态链接库中可以定义两种函数:输出函数和内部函数。输出函数可以被其他模块调用,内部函数只能被动态链接库本身调用。动态链接库也可以输出数据,但这些数据通常只被它自己的函数所使用。
库是一种软件组件技术,库里面封装了数据和函数。
库的使用可以使程序模块化。
Windows系统包括静态链接库(.lib文件)和动态链接库(.dll文件)。
Linux通常把库文件存放在/usr/lib或/lib目录下。
Linux库文件名由:前缀lib、库名和后缀3部分组成,其中动态链接库以.so最为后缀,静态链接库通常以.a作为后缀。
在程序中使用使用静态库和动态库时,他们载入的顺序是不同的。
静态库的代码在编译时就拷贝的应用程序中,这样的优点是节省编译时间。
动态链接库时程序在开始运行后调用库函数时才被载入。
创建静态库
1.编写静态库用到的函数
mylib.h
1 #ifndef _MYLIB_H_
2 #define _MYLIB_H_
3 void weclome(void);
4 void outString(const char *str);
5 #endif
mylib.c
1 #include "mylib.h"
2 #include <stdio.h>
3
4 void welcome(void)
5 {
6 printf("welcome to libmylib\n");
7 }
8 void outString(const char *str)
9 {
10 if(str != NULL)
11 printf("%s\n", str);
12 }
test.c
1 #include "mylib.h"
2 #include <stdio.h>
3 int main(void)
4 {
5 printf("create and use library:\n");
6 welcome();
7 outString("it's successful\n");
8 return 0;
9 }
2.编译mylib.c生成目标文件
gcc -c mylib.c -o mylib.o
3.将目标文件加入到静态库中
ar rcs libmylib.a mylib.o
4.将静态库复制到Linux的库目录(/usr/lib或/lib)下,而mingw是放在mingw32/lib/gcc/***/4.8.1/ 中。
gcc test.c -o test.exe -lmylib #这里的mylib是 libmylib.a后缀a和前缀lib都不用写
如果不想放在库目录可以通过-L参数进行指定。
gcc test.c -o test.exe -lmylib -L . #(这里的点表示当前目录)
连接生成后的test.exe 目录下就可以不用有libmylib.a这个文件了。
创建动态库
1.生成目标文件,然后生成动态库,要加编译器选项-fpic和链接器选项-shared
gcc -fpic -c mylib.c -o mylib.o #生成中间目标文件
gcc -shared -o libmylib.so mylib.o #生成动态库
也可以使用一步完成
gcc -fpic -shared mylib.c -o libmylib.so
2.使用动态链接库
在编译程序时,使用动态链接库和静态库是一致的,使用”-l库名”的方式,在生成可执行文件的时候会链接库文件。
gcc -o test.exe test.c -L ./ -lmylib
-L指定动态链接库的路劲,-lmylib链接库函数mylib。-lmylib是动态库的调用规则。Linux系统下的动态库命名方式是lib*.so,而在链接时表示位-l*,*是自己命名的库名。
但是程序会提示错误。
这是因为程序运行时没有找到动态链接库造成的。程序编译时链接动态库和运行时使用动态链接库的概念是不同的,在运行时,程序链接的动态链接库需要在系统目录下才行。
使用以下方法可以解决此问题
a. 在linux下最方便的解决方案是拷贝libmylib.so到绝对目录 /lib 下(但是,要是超级用户才可以)。就可以生成可执行程序了
b.第二种方法是:将动态链接库的目录放到程序搜索路径中,可以将库的路径加到环境变量LD_LIBRARY_PATH中实现(一般放在当前目录)
3.再次使用动态链接库
动态库的分为隐式调用和显式调用(上面那种)两种调用方法:
隐式调用的使用使用方法和静态库的调用差不多,具体方法如下:
gcc -c -I . test.c
gcc -o main.exe -L . Test.o libmylib.so
此时把main.exe移动到其他目录就会出现这个情况。(可以把libmylib.so移动到系统的lib目录就不会出现丢失so文件的问题)
而test.exe这个通过静态库的就没有这个问题。
使用的环境是mingw32+gcc 4.8.1
参考资料:
http://www.cnblogs.com/lidan/archive/2011/05/25/2239517.html
http://blog.sina.com.cn/s/blog_56d8ea900100xy1l.html
http://www.oschina.net/question/54100_32476
本文地址(转载注明出处):http://www.cnblogs.com/wunaozai/p/3707842.html
作者:无脑仔的小明 出处:http://www.cnblogs.com/wunaozai/ 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。 如果文中有什么错误,欢迎指出。以免更多的人被误导。有需要沟通的,可以站内私信,文章留言,或者关注“无脑仔的小明”公众号私信我。一定尽力回答。 |