iT邦幫忙

6

由GCC了解C語言,學習筆記

gcc

gcc

這邊將介紹如何使用gcc將C語言程式碼編譯成可執行程式,以下將會介紹編譯的過程,程式控制的基本選項和參數,gcc警告選項,編譯器優化等...
gcc(GNU Compiler Collection)為跨平台編譯程式,作用為把.c檔編譯成一個執行檔,總共會執行以下四個步驟

  1. 預處理(preprocessing),產生 .i 的檔案
  2. 編譯(compiling)將預處理的檔案組譯成組合語言, 產生 .s 的檔案
  3. 組譯(assembling)將組合語言變成機器碼 .o 的檔案
  4. 連接(linking)連接函式庫與其他檔案,產生可執行檔
    而gcc發展至今,不僅支援C語言,有支援C++, Java, Objective-C等等,因此,gcc由原本GNU C Compiler,逐漸轉變為GNU Compiler Collection。

嚴格說起來,gcc並不是編譯器,由他的全名GUN Compiler Collection可知道他是種編譯器套裝,概念上更像是種Compiler driver,gcc可以啟動連接器(linker),組譯器(assembler)等元件。

使用gcc編譯C語言程式

執行gcc時,預設是將一個檔案產生一個可執行檔案,如以下範例

$ gcc -Wall hello.c

hello.c的檔案內容如下

#include <stdio.h>

int main(void)
{
    printf("Hello World");
}

上面的指令包含了編譯器名稱gcc,來源檔案hello.c,和編譯選項-Wall,-Wall表示編譯器會跳出警告訊息。如果hello.c沒有語法錯誤,則gcc執行完畢後會退出,並產生一個名稱為a.out的可執行檔案(如果在windows平台則為a.exe),我們可以直接執行這個文件

$ ./a.out

輸出結果

Hello World

如果不希望產生的執行檔名稱為a,我們可以使用'-o'這個選項來指定檔案名稱

$ gcc -Wall -o hello hello.c

預處理(preprocessing)

將C語言程式碼交給編譯器之前,預處理器會先展開原始檔案中的巨集,正常情況下,gcc不會保留預處理階段所產生的檔案,但是我們可以透過-E這個選項來保留預處理時所產生的檔案。

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

此時hello.i的內容如下

# 1 "hello.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "hello.c"
# 1 "c:\\mingw\\include\\stdio.h" 1 3
# 38 "c:\\mingw\\include\\stdio.h" 3
       
# 39 "c:\\mingw\\include\\stdio.h" 3
# 56 "c:\\mingw\\include\\stdio.h" 3
# 1 "c:\\mingw\\include\\_mingw.h" 1 3
# 55 "c:\\mingw\\include\\_mingw.h" 3
       
# 56 "c:\\mingw\\include\\_mingw.h" 3
# 66 "c:\\mingw\\include\\_mingw.h" 3
# 1 "c:\\mingw\\include\\msvcrtver.h" 1 3
# 35 "c:\\mingw\\include\\msvcrtver.h" 3
       
# 36 "c:\\mingw\\include\\msvcrtver.h" 3
# 67 "c:\\mingw\\include\\_mingw.h" 2 3



# 1 "c:\\mingw\\include\\w32api.h" 1 3
# 35 "c:\\mingw\\include\\w32api.h" 3
       
# 36 "c:\\mingw\\include\\w32api.h" 3
# 59 "c:\\mingw\\include\\w32api.h" 3
# 1 "c:\\mingw\\include\\sdkddkver.h" 1 3
# 35 "c:\\mingw\\include\\sdkddkver.h" 3
       
# 36 "c:\\mingw\\include\\sdkddkver.h" 3
# 60 "c:\\mingw\\include\\w32api.h" 2 3
# 74 "c:\\mingw\\include\\_mingw.h" 2 3
# 57 "c:\\mingw\\include\\stdio.h" 2 3
# 69 "c:\\mingw\\include\\stdio.h" 3
# 1 "c:\\mingw\\lib\\gcc\\mingw32\\6.3.0\\include\\stddef.h" 1 3 4
# 216 "c:\\mingw\\lib\\gcc\\mingw32\\6.3.0\\include\\stddef.h" 3 4

# 216 "c:\\mingw\\lib\\gcc\\mingw32\\6.3.0\\include\\stddef.h" 3 4
typedef unsigned int size_t;
# 328 "c:\\mingw\\lib\\gcc\\mingw32\\6.3.0\\include\\stddef.h" 3 4
typedef short unsigned int wchar_t;
# 357 "c:\\mingw\\lib\\gcc\\mingw32\\6.3.0\\include\\stddef.h" 3 4
typedef short unsigned int wint_t;
# 70 "c:\\mingw\\include\\stdio.h" 2 3
# 94 "c:\\mingw\\include\\stdio.h" 3
# 1 "c:\\mingw\\include\\sys/types.h" 1 3
# 34 "c:\\mingw\\include\\sys/types.h" 3
       
# 35 "c:\\mingw\\include\\sys/types.h" 3
# 62 "c:\\mingw\\include\\sys/types.h" 3
  typedef long __off32_t;

  typedef __off32_t _off_t;



  typedef _off_t off_t;
# 91 "c:\\mingw\\include\\sys/types.h" 3
  typedef long long __off64_t;


  typedef __off64_t off64_t;
# 115 "c:\\mingw\\include\\sys/types.h" 3
  typedef int _ssize_t;



  typedef _ssize_t ssize_t;
# 95 "c:\\mingw\\include\\stdio.h" 2 3



# 1 "c:\\mingw\\lib\\gcc\\mingw32\\6.3.0\\include\\stdarg.h" 1 3 4
# 40 "c:\\mingw\\lib\\gcc\\mingw32\\6.3.0\\include\\stdarg.h" 3 4
typedef __builtin_va_list __gnuc_va_list;
# 103 "c:\\mingw\\include\\stdio.h" 2 3
# 210 "c:\\mingw\\include\\stdio.h" 3
typedef struct _iobuf
{
  char *_ptr;
  int _cnt;
  char *_base;
  int _flag;
  int _file;
  int _charbuf;
  int _bufsiz;
  char *_tmpfname;
} FILE;
# 239 "c:\\mingw\\include\\stdio.h" 3
extern __attribute__((__dllimport__)) FILE _iob[];
# 252 "c:\\mingw\\include\\stdio.h" 3

...略

 __attribute__((__cdecl__)) __attribute__((__nothrow__)) wint_t fgetwchar (void);
 __attribute__((__cdecl__)) __attribute__((__nothrow__)) wint_t fputwchar (wint_t);
 __attribute__((__cdecl__)) __attribute__((__nothrow__)) int getw (FILE *);
 __attribute__((__cdecl__)) __attribute__((__nothrow__)) int putw (int, FILE *);


# 2 "hello.c" 2


# 3 "hello.c"
int main(void)
{
    printf("Hello World");
}

我們可以發現所謂的預處理就是將stdio.h這個標頭檔展開,也就是將stdio.h內所有的函式定義加入到hello.c中,而我們在hello.c呼叫了printf函式,就會尋找到由stdio.h所展開的printf函式定義,並進行呼叫

而我們也發現了這麼做的缺點,就是stdio.h過於龐大,以至於我們難以閱讀,因此,加入-C的選項,這個參數可以阻止預處理器刪除源檔案或是標頭檔的註解,當我們引入許多標頭檔時。

$ gcc -E -C -o hello.i hello.c

編譯(compiling)

編譯器的主要任務是將.c檔翻譯成組合語言(assembly language)。組合語言是人類可以讀懂的語言,也是最接近機器碼的語言。組合語言會因為CPU的架構而有所不同。

組合語言(assembly language)本質上計算機二進位編碼機器語言(machine language)的符號呈現方式。組合語言因為使用的是符號而非一長串的位元組因此較方便閱讀。組合語言名稱裡的符號一般來說會代表一些位元形式,像是暫存器名稱等等,以便人們閱讀和記憶。此外,組合語言可以讓程式開發者使用標籤(labels)來標註存放特定指令或是數據的記憶體地址。

一般情況下,gcc會將組合語言輸出並儲存到暫存檔案中,並在組譯器執行結束後立即刪除。
但是,我們可以透過-S選項讓組合語言的檔案產生後立即停止

$ gcc -S hello.c

以下為hello.s的內容

	.file	"hello.c"
	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
LC0:
	.ascii "Hello World\0"
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
LFB10:
	.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	$16, %esp
	call	___main
	movl	$LC0, (%esp)
	call	_printf
	movl	$0, %eax
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
LFE10:
	.ident	"GCC: (MinGW.org GCC-6.3.0-1) 6.3.0"
	.def	_printf;	.scl	2;	.type	32;	.endef

編譯器預處理hello.c,將其翻譯為組合語言,並將結果儲存在hello.s中,如果我們想把C語言變數的名稱作為組合語言裡的註解,可以加上-fverbose-asm這個參數,方便我們閱讀
我們以以下程式,Sum.c作為示範

int main(void)
{
    int a = 2;
    int b = 3;
    printf("%d", a + b)
}

輸入指令

$ gcc -S -fverbose-asm Sum.c

Sum.S的內容如下

	.file	"Sum.c"
 # GNU C11 (MinGW.org GCC-6.3.0-1) version 6.3.0 (mingw32)
 #	compiled by GNU C version 6.3.0, GMP version 6.1.2, MPFR version 3.1.5, MPC version 1.0.3, isl version 0.15
 # GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
 # options passed:  -iprefix c:\mingw\bin\../lib/gcc/mingw32/6.3.0/ hello.c
 # -mtune=generic -march=i586 -fverbose-asm
 # options enabled:  -faggressive-loop-optimizations
 # -fasynchronous-unwind-tables -fauto-inc-dec -fchkp-check-incomplete-type
 # -fchkp-check-read -fchkp-check-write -fchkp-instrument-calls
 # -fchkp-narrow-bounds -fchkp-optimize -fchkp-store-bounds
 # -fchkp-use-static-bounds -fchkp-use-static-const-bounds
 # -fchkp-use-wrappers -fcommon -fdelete-null-pointer-checks
 # -fdwarf2-cfi-asm -fearly-inlining -feliminate-unused-debug-types
 # -ffunction-cse -fgcse-lm -fgnu-runtime -fgnu-unique -fident
 # -finline-atomics -fira-hoist-pressure -fira-share-save-slots
 # -fira-share-spill-slots -fivopts -fkeep-inline-dllexport
 # -fkeep-static-consts -fleading-underscore -flifetime-dse
 # -flto-odr-type-merging -fmath-errno -fmerge-debug-strings -fpeephole
 # -fplt -fprefetch-loop-arrays -freg-struct-return
 # -fsched-critical-path-heuristic -fsched-dep-count-heuristic
 # -fsched-group-heuristic -fsched-interblock -fsched-last-insn-heuristic
 # -fsched-rank-heuristic -fsched-spec -fsched-spec-insn-heuristic
 # -fsched-stalled-insns-dep -fschedule-fusion -fsemantic-interposition
 # -fset-stack-executable -fshow-column -fsigned-zeros
 # -fsplit-ivs-in-unroller -fssa-backprop -fstdarg-opt
 # -fstrict-volatile-bitfields -fsync-libcalls -ftrapping-math
 # -ftree-cselim -ftree-forwprop -ftree-loop-if-convert -ftree-loop-im
 # -ftree-loop-ivcanon -ftree-loop-optimize -ftree-parallelize-loops=
 # -ftree-phiprop -ftree-reassoc -ftree-scev-cprop -funit-at-a-time
 # -funwind-tables -fverbose-asm -fzero-initialized-in-bss -m32 -m80387
 # -m96bit-long-double -maccumulate-outgoing-args -malign-double
 # -malign-stringops -mavx256-split-unaligned-load
 # -mavx256-split-unaligned-store -mfancy-math-387 -mfp-ret-in-387
 # -mieee-fp -mlong-double-80 -mms-bitfields -mno-red-zone -mno-sse4
 # -mpush-args -msahf -mstack-arg-probe -mstv -mvzeroupper

	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
LC0:
	.ascii "%d\0"
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
LFB10:
	.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)	 #, a
	movl	$3, 24(%esp)	 #, b
	movl	28(%esp), %edx	 # a, tmp90
	movl	24(%esp), %eax	 # b, tmp91
	addl	%edx, %eax	 # tmp90, _3
	movl	%eax, 4(%esp)	 # _3,
	movl	$LC0, (%esp)	 #,
	call	_printf	 #
	movl	$0, %eax	 #, _6
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
LFE10:
	.ident	"GCC: (MinGW.org GCC-6.3.0-1) 6.3.0"
	.def	_printf;	.scl	2;	.type	32;	.endef

可以看到組合語言中每一個暫存器處理後面會加上在C語言中的變數名稱

組譯(assembling)

每個處理器架構都有自己的組合語言,gcc呼叫組譯器(assembler),把組合語言翻譯成可執行的二進位碼,產生出的檔案為一個目的檔(object file),其中包含了機器碼用來執行源檔案所定義的函數,還包含了一個符號表(symbol table),用來描述源檔案中具有外部鏈接的所有對象,包括函式,變數等等。

:::info
組譯器(assembler) : 組譯器將組合語言翻譯成以二進位制呈現的指令串。組譯器讀入一個由編譯產生的來源檔(由組合語言所組成),並產生出一個目的檔,裏頭包含機器指令,以及一個有助於合併其他目的檔的符號表。
:::
如果呼叫gcc同時編譯和鏈結一個程式,中間會產生出一個目的碼(object file),在鏈接器執行完之後便會自動刪除。然而,一般來說,編譯和鏈結的工作通常是分開進行的。使用-c參數表示gcc不會鏈接函式庫,但會對每一個輸入的檔案產生目的碼,也就是.o檔。

$ gcc -c hello.c

我們可以試著檢視hello.o檔的內容
我們會看見一堆不知道是什麼的內容,但其中還是有一些有用的資訊

一般來說,UNIX中的目的檔含有六個不同的部分

  1. 目的檔頭(object file header) : 說明這個檔案其餘部分的大小和位置
  2. 文字部分(text segment) : 含有來源檔案中各程序的機器碼。這些程序因為含有許多未解決的參考而可能無法執行,也就是尚未連接函式庫,下方鏈接部分會加以說明
  3. 數據部分(data segment) : 含有來源檔中數據的二進位表示形式。數據也可能因為含有對其他檔案中標籤的未解決的參考而不完整
  4. 重置資訊(relocation information) : 表示有哪一些指令和數據字組(bytes)是和絕對位址(absolute addresses)有關的。如果程式的某些部份在一體中被移動了,則這些參考一定也要做出相對應的改變,避免指令或數據遺失的情況發生。
  5. 符號表(symbol table) : 將位址和來源檔中的外部標籤做連結,並列出未解決的參考。
    :::info
    補充: 實際上,gcc是個適用於多種CPU架構的編譯器,不會直接把C語言直接轉換為組合語言,而是在這兩者之間,輸出成一種中間語言,稱為暫存器傳輸語言(Register Transfer Language),簡稱RTL
    :::

我們可以使用file這個指令去查看檔案的資訊,以下示範查看hello.c, hello.o, hello.out的屬性

我們可以觀察到,test.o為EL 64-bit LSB relocatable等資訊,以下解釋這一些資訊的意義:

ELF : 代表這是可執行檔的格式(ELF代表Executable and Linking Format,表示可執行或是可連結的格式)

64-bit : 代表每一個字節(bytes)的長度

LSB shared object : 表示以最小有效字節(least significant byte)的順序進行編譯,例如Intel和AMD的x86處理器

version 1 (SYSV) : 表示檔案內部的格式版本

dynamically linked : 表示使用動態連接,也就是使用共享的函式庫(與使用-static選項的檔案不同)

not stripped : 表示在test.o中包含符號表(symbol table)

我們可以使用gcc中的-Wa選項,將指令選項傳遞給組譯器,例如,我們希望組譯器使用以下選項:
-as=hello.sym: 輸出符號表並儲存到hello.sym中
-L表示在符號表中,需要包含

我們可以在gcc呼叫組譯器時加上這一些選項,方法如下

$ gcc -v -o hello -Wa,-as=hello.sym,-L hello.c

-v表示讓gcc輸出在每一個編譯步驟時,所使用的檔案資訊

大多數的程式會依功能或是資料結構猜分成許多個檔案,分別進行編寫,編譯與組譯,這樣的檔案我們稱為一個模組(modules)。程式也可以使用預先寫好放在程式函數庫(program library)裡面的程式。一個模組通常會包含參考(references),參考定義為到其他模組或是程式函數庫取得數據或是函式的動作。

如果參考的其他模組或是函數庫在執行的時候發現無法進行存取,也就是無法進行參考,則這種參考我們會稱為未解決的參考(unresolved reference),而要使參考能夠順利進行,我們會使用鏈接器(linker)這個工具,幫助程式連接其他函式庫以順利執行,變成一個可執行檔(executable file)

符號表(symbol table)

前面有說到,一個object file會包含一個符號表,符號表確切的內容為按照函式和變數的名稱儲存他們所在的記憶體地址,也就是前面所說到的,用來描述檔案中具有外部的連接對象。
我們將test.o的符號表輸出,並檢視其內容test.sym

DEFINED SYMBOLS
                            *ABS*:0000000000000000 test.c
     /tmp/ccVVwC4o.s:4      .rodata:0000000000000000 .LC0
     /tmp/ccVVwC4o.s:9      .text:0000000000000000 main
     /tmp/ccVVwC4o.s:10     .text:0000000000000000 .LFB0
     /tmp/ccVVwC4o.s:26     .text:0000000000000020 .LFE0

UNDEFINED SYMBOLS
_GLOBAL_OFFSET_TABLE_
printf

我們也可以用'nm'這個指令,來查看執行檔的符號表,以hello.out為例

0000000000004010 B __bss_start
0000000000004010 b completed.0
                 w __cxa_finalize@@GLIBC_2.2.5
0000000000004000 D __data_start
0000000000004000 W data_start
0000000000001090 t deregister_tm_clones
0000000000001100 t __do_global_dtors_aux
0000000000003dc0 d __do_global_dtors_aux_fini_array_entry
0000000000004008 D __dso_handle
0000000000003dc8 d _DYNAMIC
0000000000004010 D _edata
0000000000004018 B _end
00000000000011e8 T _fini
0000000000001140 t frame_dummy
0000000000003db8 d __frame_dummy_init_array_entry
000000000000215c r __FRAME_END__
0000000000003fb8 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
0000000000002010 r __GNU_EH_FRAME_HDR
0000000000001000 t _init
0000000000003dc0 d __init_array_end
0000000000003db8 d __init_array_start
0000000000002000 R _IO_stdin_used
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
00000000000011e0 T __libc_csu_fini
0000000000001170 T __libc_csu_init
                 U __libc_start_main@@GLIBC_2.2.5
0000000000001149 T main
                 U printf@@GLIBC_2.2.5
00000000000010c0 t register_tm_clones
0000000000001060 T _start
0000000000004010 D __TMC_END__

在符號表中,我們可以看到main函式有0x01149的十六進位的偏移量(offset),上面大多數的符號是提供給編譯器和作業系統使用,像是'T'表示在目的檔中定義的函式,'U'表示未定義的函式(需要透過鏈結其他目的檔來解決,像是 printf@@GLIBC_2.2.5這一行)。

'nm'最常見的應用是檢查函式庫中是否包含一些特殊的函式定義,我們可以透過查看符號'T'去達成這一件事情。

鏈接(linking)

鏈接就是把多個二進位的目的檔(object file)鏈接成一個單獨的可執行檔,在鏈接的過程中,會將符號表中符號對應到的函式或是變數以實際的記憶體地址替換,使程式完成外部引用(像是呼叫外部檔案或是外部函式等等)。鏈接器使用組譯器中符號表提供的訊息來完成上面這一些動作。

鏈接器也必須將程式中所用到的所有C標準函式庫也加入到其中。

標準函式庫的大部分函數通常是放在 libc.a裡面 (.a代表"achieve"),也被稱為靜態函式庫 ,或者是放在共享物件動態函式庫 libc.so (.so代表"share object") 。這一些函式庫一般會在/lib/或是/usr/lib/裡面,或是gcc預設搜索的其他函式庫目錄。

共享物件函式庫是在程式執行時,才由動態連接器載入到程式的記憶體空間中供程式呼叫,因此不屬於應用程式的一部分。不過在編譯的時候,它們必須要是可以連結使用的。

一般來講,共享物件函式庫是類似 Windows 上的 DLL 檔案。

所以,一般常聽到的DLL錯誤,原因是因為缺少函式庫,或是沒有被動態連接器載入到程式的記憶體空間中呼叫,導致的錯誤。

如果我們因為一些需要,希望用到C標準函式庫以外的函式庫,我們需要加上'l'這個選項,連接外部函式庫,以供程式呼叫與使用

$ gcc -o hello hello.c -loutside

表示連接外部函數庫,函數庫名稱為outside,而連接庫的檔案名稱為liboutside.a,一般來說,都會加上前綴lib和一個後綴副檔名.a。

分別編譯(separate compilation)允許將一個大型程式猜分成許多檔案,每個檔案可能是大型程式所需要用到的資料結構,或是一些函式。每個檔案可以分別獨立進行編譯或是組譯,因此改動內容時不需要編譯整個大型程式,只需要編譯改動的檔案。如同上面所說,邊別編譯需要額外的鏈接過程去合併不同模組的目的檔(object file),並釐清他們之間存在的未解決的參考,而用來合併這些檔案所使用到的工具即為鏈接器(linker)。

通常gcc會在標準函式庫的目錄中搜尋函式庫檔案,例如/usr/lib。有三種方式可以鏈接標準函式庫路徑以外的函式庫。

  1. 把函式庫當作一般檔案進行處理,為gcc指定該函式庫的完整路徑和檔案名稱,舉例來說,如果函式庫名稱為liboutside.a,且路徑為/usr/local/lib,那麼下面的指令便可以讓gcc編譯hello.c,然後將liboutside.a鏈接到hello.o。
$ gcc -o hello hello.c /usr/local/lib/liboutside.a

這個例子中,函式庫必須放在源程式碼檔案名稱的後面,原因為鏈接器會根據指令從左到右進行處理

  1. 使用gcc選項-L去搜索標準函式庫路徑以外的函式庫,指令如下
$ gcc -o hello -L/usr/local/lib -loutside hello.c
  1. 將想要連接的函式庫目錄加到環境變數LIBRARYPATH裡面,把選項傳遞到鏈接器中,可以使用Wl這個選項,後面加上','區分傳入的參數,使用這個方式可以直接把選項傳遞到鏈接器,指令如下
$ gcc -loutside -Wl,-Map,hello.map hello.c 

關於動態共享函式庫與靜態函式庫

動態共享連結函式庫(Shared library)是在程式開始執行時才載入的,具有三個優點

  1. 減少執行檔大小
  2. 更新函式庫時無須重新編譯其他程式
  3. 可在程式執行時修改函式庫

靜態函式庫(Static library)是在程式變為執行檔時便載入完成,具有三個優點

  1. 較高的執行速度
  2. 只要保證使用者有程式對應的函式庫,便能執行
  3. 避免因為程式找不到.dll而無法執行(dll地獄)

每個動態函式庫都會以lib作為函式庫的開頭名稱,然後加上函式庫名稱,末端加上副檔名.a或是.so,其中.so表示這是一個共享函式庫,.a表示這是一個靜態函式庫,如果我們是使用靜態函式庫,那麼在程式編譯時會將函式庫中的元件鏈接到執行檔中,造成執行檔的大小較大,好處是我們執行程式時,就不需要該函式庫了。

在windows平台中,靜態函式庫的副檔名為.lib,動態函式庫的副檔名為.dll

靜態函式庫 : 當程式使用靜態函式庫時,源程式檔案所使用到函式庫中所有函式的機器碼會被複製到最終的可執行檔中,這會導致最終生成執行檔的程式碼變多,但好處在於我們在執行程式時便不需要再去呼叫函式庫,速度上會比較快,但假設今天有多支程式會使用到這個函式庫,這個函式庫就會分別複製到每一個執行檔中,造成更多記憶體空間的消耗。

動態共享函式庫 : 與共享函式庫練街的可執行檔只需要包含他需要的函式引用表(紀錄於符號表中),而不是整個函式庫的機器碼。在程式執行時用到的函式就去參考符號表去對函式庫進行呼叫即可,優點為執行檔檔案較小,節省硬碟空間,甚至如果使用虛擬記憶體,可以將動態函式庫載入到虛擬記憶體中,供多個程式進行呼叫,也就是所謂的共享,解決靜態函式庫的記憶體消耗問題,不過缺點為執行時會需要去鏈接函式庫,需要額外時間,執行速度會慢一些。

產生動態/靜態函式庫

可以使用gcc中的-share選項,輸入的檔案必須是一個已經存在的.c檔,以下範例

$ gcc -c hello.c

產生出目的檔,接著執行

$ gcc -shared -o libhello.so hello.o

這樣我們便成功建立出一個動態共享函式庫的檔案了,我們可以把一個檔案和這個函式庫進行鏈接

$ gcc -c hello_1.c
$ gcc -o hello_1 hello_1.o libhello.so -loutside

上面的指令會建立一個可執行檔案,在執行時會動態的連接到libhello.so。我們必須確保程式在執行時可以找到這個動態共享連接函式庫,我們可以將函式庫放在標準函式庫的目錄底下,或是使用環境變數進行設置。

如果我們不想使用動態共享函式庫,我們有兩種方式可以產生出靜態鏈接的執行檔,一種方法為使用static這個選項

$ gcc -static -o hello hello.o hello_1.o -loutside

或是

$ gcc -o hello hello.o hello_1.o /usr/lib/liboutside.a

直接指定到外部函式庫

輸出所有步驟的檔案

gcc中有一個選項是我們可以把編譯,組譯,鏈接的中間檔案全部輸出到目前的目錄地下,選項為-save-temps,當使用這個選項時,gcc會正常地進行編譯和鏈接,但是會把預處理器的輸出,組合語言和目的檔全部輸出在目前的目錄底下,副檔名分別為.i, .s, .o

  1. .c : C語言程式碼。
  2. .i : C語言程式碼的預處理輸出,可以被編譯。
  3. .h : C語言標頭檔。
  4. .s : 組合語言檔。
  5. .S : 同樣也是組合語言檔案,與.s差別為後期會再進行預處理和組譯。

main函式與crt

對於一個執行檔來說,除了鏈接目的檔和函式庫檔案以外,鏈接器還需要鏈接系統的啟動碼,或是稱為入口函式,程式需要這個啟動碼才能夠被載入到記憶體中。而這個入口函式被放在一個標準目的檔crt0.o裡面,crt為c runtime的簡寫,裡面包含執行檔實際的進入點,大多數的系統中,gcc在預設情況下會鏈接兩個目的檔,分別為crtbegin.o和crtend.o。

如果我們想要寫一個獨立程式,不鏈接gcc的啟動碼,我們可以使用-nostdlib這個選項來達成這一件事情,禁止程式鏈接到C的標準函式庫。在這個情況下,C語言程式就不需要從main()開始,可以使用gcc中ename來指定其他的進入點。

crt為c runtime的縮寫,描述進入main函式之前的初始化和退出main函式的清理工作。

crt0.o,又稱為c0,是鏈接到C語言程式的啟動函式,他們不是函式庫,比較像是內核的組合語言,負責進行程式在進入main函式以前的工作,包含初始化程式的stack,設置中斷請求等等工作,而後面加入了建構函式和解構函式等功能之後被稱為crt1.o。

C Run-Time Library裡面含有初始化代碼,還有錯誤處理代碼(例如divide by zero處理)。我們有時候寫程式時即便沒有stdio.h也可以執行printf,是因為函式庫中有這個函式,而gcc會預設自動鏈接C Run-Time Library,但如果缺少或是不去鏈接C Run-Time Library,程式便無法進入main函式中。C Run-Time Library就是一個包含C語言運作的最基本和最常用的函式的函式庫。

crt1.o, crti.o, crtbegin.o, crtend.o, crtn.o 等目的檔和hello.o鏈接產生執行檔。
這五個目的檔分別功用為啟動,初始化,建構,解構和結束
在標準的linux系統上,鏈接的順序為ld crt1.o crti.o [user_objects] [system_libraries] crtn.o

crt1.o和crti.o以及crtn.o為C語言的啟動函式,而crtbegin.o和crtend.o為C++的啟動函式

crt1.o裡面包含了進入函式_start和兩個為定義符號__libc_start_main和main,由start呼叫 __libc_start_main初始化libc,然後呼叫我們程式碼中main函式。

如果我們程式對一個程式不去鏈接標準函式庫,會出現以下錯誤訊息

找不到_start符號而發生錯誤,是缺少_start,而不是main

編譯器警告

當我們進行編譯時,可能gcc會像我們傳遞兩種訊息,一種是錯誤訊息(error message),如果產生錯誤訊息,則程式無法順利完成編譯。如果是警告訊息(warning),則是通知我們需要修改和了解某一些地方(像是遵循函式定義的標準,如sizeof回傳值使用int的形式進行printf等),但警告並不會阻礙編譯器完成編譯。

gcc可以使用某一些選項對於警告訊息進行一些控制,例如使用-Werror,讓gcc在遇到任何警告訊息時都自動停止編譯。還有一些選項,可以請求編譯器再碰到一些模糊,不嚴謹的語法時發出警告。例如透過-W開頭的選項,來一個個啟用大部分gcc警告,像是當使用switch時,如果沒有default時,則Wswitch-default會讓gcc產生出警告訊息。

而要讓gcc產生出大部分的警告,我們可以使用-Wall這個選項,但這個選項並不能產生出所有的警告,還有許多的選項需要獨立啟用,像是-Wshadow。如果使用-Wall這個選項,但是想要忽略或是取消其中部分的警告,可以使用-Wno...這個選項,像是-Wno-switch-default就會關閉switch缺少default的警告,如果要關閉所有警告,則使用'-w(小寫)即可關閉所有警告。

編譯器優化

gcc採用了許多的技術使得產生的執行檔大小變的更小,執行的速度更快。這一些經過優化所產生出的程式碼往往使除錯和測試變得更加不容易,因此,一般情況會將程式經過測試和除錯之後,才會進行優化。

gcc中有兩類優化選項,其中一種為-f(代表flag)選項,指示要採用的優化技術,例如-fmerge-constants選項會讓編譯器將一樣的常數放在同一個位置,甚至可以用在不同檔案間一樣的常數。我們也可以使用-O選項,設定優化的等級,可以一次使用多種優化技術。

-O選項 優化等級

每個-O選項代表多個優化技術的集合,-O優化等級是遞增的: -O2的優化包含-O1所提供的優化技術以及其他優化,-O3也是如此,下面簡單介紹-O選項的等級

  1. -O0 關閉所有優化選項
  2. -O1 會嘗試減少程式碼,和增加程式的執行效率,減少執行時間,但不會花費太多的編譯時間在執行優化。主要針對常數和表示式進行優化。如果-O後面沒有數字,就等同於-O1
  3. -O2 應用幾乎所有的優化技術,但不會做空間和時間取捨問題的優化,會花費更多的編譯時間進行優化。主要針對暫存器和一些指令的優化
  4. -O3 包含了所有-O2的優化技術,產生inline函式,並讓變數在CPU暫存器中更加靈活的分配使用空間。
  5. -Os 主要優化程式大小,但不包含任何可能導致程式碼量增加的優化技術。

優化可能導致的問題

優化會導致編譯時花費更多的時間,且優化會改變程式碼的結構,以及記憶體的操作順序等等(-O2優化會發生),會導致我們難以除錯,追蹤變數。因為這些理由,所以會選擇在程式完成除錯和測試之後才會進行優化。

重新排列記憶體順序,可能會導致我們在撰寫多執行續的程式時,無法了解程式如何控制共享記憶體等等。

gcc除錯選項

使用-g選項,可以允許gcc在目的檔和執行檔中包含符號表和一些源程式碼的資訊。gdb需要符號表做為參考才能進行除錯,在除錯程式時可以使用這一些訊息,達到程式單步執行,或是顯示暫存器,記憶體中的內容。

Reading symbols from ./pointer,表示在pointer執行檔中有找到符號表,可以使用gdb進行除錯。

也可以在-g後面加上一些後綴字,產生出新的指令選項,指示產生出與系統原始格式不同的符號表來儲存訊息,像是-ggdb表示產生出最適用於gdb的符號表格式來進行除錯。

效能分析

使用-p選項會在程式中加入一些特殊的函式,可以讓程式在執行時輸出剖析訊息(profiling information),這些訊息可以讓我們解決一些有關效能的問題,函式消耗的時間等等。剖析的訊息會被儲存到mon.out這個檔案中,我們可以使用prof這個工具來分析這一些訊息。

對於GNU剖析器(profiler),在編譯時可以加入-pg這個選項來啟用它,會輸出gmon.out這個檔案。gprof和-pg選項的使用,可以產生出一張呼叫圖,顯示程式內的函式是如何相互呼叫的。

參考資料

https://hackmd.io/@sysprog/c-compiler-optimization?type=view
https://www.cnblogs.com/youxin/p/7988479.html
https://www.itread01.com/content/1547104893.html
https://stackoverflow.com/questions/2709998/crt0-o-and-crt1-o-whats-the-difference
https://hackmd.io/@sysprog/c-runtime?type=view
https://zh.wikipedia.org/zh-tw/DLL%E5%9C%B0%E7%8D%84
https://zh.wikipedia.org/wiki/%E5%8A%A8%E6%80%81%E9%93%BE%E6%8E%A5%E5%BA%93
https://linoxide.com/gprof-performance-analysis-programs/
C語言核心技術,第二版

新手發文,請多指教


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
Alan
iT邦新手 5 級 ‧ 2022-08-04 13:35:03

獲利良多,讚!

感謝您的回應,能給予您幫助是我的榮幸!

0
JC
iT邦新手 2 級 ‧ 2022-12-13 14:03:21

推 感謝分享
查資料無意間發現的
原來 Preprocessor 還有 .i 檔

感謝回應
.i 檔是在查看 gcc 指令的時候無意發現的
/images/emoticon/emoticon01.gif

我要留言

立即登入留言