大一的時候,學習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