GCC-程序編譯過程

本文以gcc爲編譯工具記錄下程序的編譯過程


編譯(總)

當我們寫好一個簡單的helloword程序,需要把程序轉化爲可執行文件時,需要經歷的一個步驟就是編譯。

hello.c
-----------------------------------------------
#include <stdio.h>

#define X 1

int main()
{
        int i=0;
        printf("hello world X = %d i=%d !\n",X,i);
        return 0;
}

如果使用的編譯工具是gcc,只需要一個 gcc hello.c 便可以編譯完輸出a.out文件。但是簡單的編譯中包含着幾個步驟--預編譯、編譯、彙編、鏈接(嚴格說鏈接並不屬於編譯流程,這裏直接一起講了),瞭解這些步驟有助於加深對程序的理解。

預編譯

C語言中有着以'#'開頭的預處理指令,包括#ifdef 、#include和#define等都屬於預編譯指令。預編譯階段,gcc會將預編譯指令做處理,將使用到宏定義#define的地方展開成真實值,將#include的頭文件賦值到文件中等,但是不會檢查複製之後是否正確,正確性交給後續流程處理。

gcc -E -o hello.i hello.c

如上,讓gcc只執行預編譯流程需要添加 -E 選項,同時一般預編譯的輸出文件爲.i後綴。這裏節選了部分的結果文件,可以看到#include展開後相關的頭文件和聲明都被複制到該文件中,同時main函數中使用到#define X的地方也被替換成實際的1。

# 1 "hello.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "hello.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 27 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/features.h" 1 3 4
# 367 "/usr/include/features.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 1 3 4
# 410 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
# 411 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 3 4
# 368 "/usr/include/features.h" 2 3 4
# 391 "/usr/include/features.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 1 3 4
# 10 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/gnu/stubs-64.h" 1 3 4
# 11 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 2 3 4
# 392 "/usr/include/features.h" 2 3 4
# 28 "/usr/include/stdio.h" 2 3 4

...
...


extern int fprintf (FILE *__restrict __stream,
      const char *__restrict __format, ...);


extern int printf (const char *__restrict __format, ...);

...
...

# 5 "hello.c"
int main()
{
 int i=0;
 printf("hello world X = %d i=%d !\n",1,i);
 return 0;
}

編譯

在上述文件基礎上需要執行的下一個流程是編譯,這裏的編譯和本文開頭的編譯不是一回事,開頭的編譯是對本文所要講解的幾個流程的簡稱。而本處的編譯指的是將預編譯後的文件進行編譯,僅爲其中的一個流程。編譯會生成對應機器的彙編碼,同樣以hello.c預編譯完成的hello.i爲例,使用 -S 參數執行編譯流程。

gcc -S -o hello.s hello.i
        .file   "hello.c"
        .section        .rodata
.LC0:
        .string "hello world X = %d i=%d !\n"
        .text
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $16, %rsp
        movl    $0, -4(%rbp)
        movl    -4(%rbp), %eax
        movl    %eax, %edx
        movl    $1, %esi
        movl    $.LC0, %edi
        movl    $0, %eax
        call    printf
        movl    $0, %eax
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
        .section        .note.GNU-stack,"",@progbits

彙編,鏈接

彙編階段做的是將編譯流程輸出的彙編碼轉化爲二進制機器碼,但是還不能直接運行,需要鏈接上依賴的庫或者文件才能得到最終的可執行文件。彙編過程使用 -c 參數,鏈接則直接將需要鏈接的.o文件直接執行gcc即可。

gcc -c -o hello.o hello.s
gcc -o hello hello.o

示例中的鏈接僅列出了我們自己的hello.o,沒有看到其他依賴的庫,但是仍然能夠鏈接成功且能夠正常執行,這是因爲依賴的系統庫在這個過程中被默認加載了,這裏直接在鏈接時gcc中加入 -v 參數就能觀察到,

 gcc -v -o hello  hello.o
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 5.4.0-6ubuntu1~16.04.12' --with-bugurl=file:///usr/share/doc/gcc-5/README.Bugs --enable-languages=c,ada,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-5 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-5-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-5-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-5-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.12)
COMPILER_PATH=/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-o' 'hello' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-linux-gnu/5/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/5/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper -plugin-opt=-fresolution=/tmp/ccLdFZul.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --sysroot=/ --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -z relro -o hello /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o -L/usr/lib/gcc/x86_64-linux-gnu/5 -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/5/../../.. hello.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o

使用-v參數後的輸出如上,使用ctrl+f搜索可以看到 -lc 的參數出現,這是鏈接了系統的libc庫,直接使用 ldd 命令也可以看到最終生成的可執行文件hello所依賴的庫。

CryptonymAMS$: ldd hello
        linux-vdso.so.1 =>  (0x00007ffc397a6000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4a8ed33000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f4a8f0fd000)

這裏hello採用了動態鏈接鏈接到系統庫,可以使用 file 命令查看到dynamically linked,也就是動態鏈接的方式。

hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=ac17e411d6f0b61087de124ee029056f09f4c990, not stripped

至此,gcc的所有編譯流程結束,可執行文件輸出。

 

補充

gcc的常用參數

-E :執行到預編譯流程就停止,不執行後續編譯、彙編等流程
-S :只執行到編譯流程,不執行後續彙編等流程
-c :只執行到彙編流程,不執行鏈接流程

-o :指定輸出文件的名字

-I :指定頭文件路徑
-L :指定庫文件路徑
-l :指定需要鏈接的庫(如libc.so,只需要 -lc 即可)

-static :靜態鏈接方式(可執行文件包含執行需要的所有信息,但是文件大小會比較大)
-nostdlib :不鏈接標準庫

-v :查看gcc版本信息,編譯過程使用時用於打印編譯過程的輸出

更多參數可以直接man gcc或者gcc --help查看。

發佈了13 篇原創文章 · 獲贊 7 · 訪問量 6063
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章