由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语言核心技术,第二版

新手发文,请多指教


<<:  Day 28 (Jq)

>>:  Tailwind CSS 中的样式渲染顺序

Day21 密室逃脱之藏宝图

PivotTable.js Modification 承续昨天的自我对话,今天就继续来实行昨天的方...

截取Video画面,存成一张张图片Python cv2

找到一个有趣的程序码,改了一下,可截取Video画面,存成一张张图片。 进行中想要中断执行,可按 E...

AutoML NAS - SGAS: Sequential Greedy Architecture Search(上篇)

1 前言 近年来深度学习使用在许多比赛中,但几乎都使用ensemble(集成)的方式或是使用庞大的模...

追求JS小姊姊系列 Day2 -- 谁说难搞的女生没朋友?

前情提要 先帮大家回味一下,第一天我说了些哪些内容: 之前跟她装熟很失败,现在决心要打掉重练 啊,可...