嵌入式軟件開發之------淺談C代碼編譯過程

開發環境:ubuntu 16.04

編譯器:arm-linux-gnueabi-gcc 5.4.0

一、導讀

    前些天幫助同事做linux內核熱補丁,製作linux內核熱補丁需要修改後C文件編譯出來的xxx.oxxx.obj文件,然後就發現不少工作幾年的同事,一直以爲編譯就是一步完成的,不知道編譯xxx.o是怎麼產生的,尤其是公司成熟的平臺都寫好的腳本一鍵編譯,很多人就更不瞭解編譯過程是怎麼進行的。

    作爲一個嵌入式軟件開發者,雖然更多的時候是使用工具和調用API,但瞭解其原理還是必要的,在出現問題的時候不至於束手無策;但畢竟不是做工具,不需要精通每一個細節,在需要的時候再深入即可。

二、背景知識

1. CPU內部運行的時候,只有01組成的機器碼,無論是獲取指令還是數據,都是通過訪問指令或數據的地址來完成的,像我們定義的全局變量aCPU最終是通過訪問a所在的地址來獲取a的值;同樣對函數的調用,也是跳轉到函數所在的地址。所以我們C文件裏定義的變量和函數,最終都是通過其地址訪問的。

2. 在整個編譯完成後,編譯器會爲全局變量和函數分配地址,這個也叫做編譯地址,編譯地址可在生成的xxx.map文件中查看。當CPU實際運行時,全局變量和函數所在的實際地址叫做運行地址,所以程序要正確的運行,就需要實際運行地址和編譯地址對應,否則可能會出錯(地址無關代碼不會出錯,像Uboot開始的一段代碼就地址無關)。比如編譯器將變給變量a分配0x100地址,那麼訪問a的指令都會到0x100來取值,如果運行時,運行地址和編譯地址不等,a被放到0x200,那麼原來訪問0x100的指令就會取錯值。

3. gcc編譯產生的xxx.o和執行文件,都是ELF文件格式中的其中一種(當然還有別的格式,可自行百度),因爲重點在編譯過程,所以可以只關注textdatabssrelocationsection

三、編譯過程概要

從一個C代碼文件到可執行文件需要經過以下四個過程。

預處理(Preprocessing)   

編譯(Compilation)

彙編(Assembly)

鏈接(Linking)

gcc編譯選項

root@ubuntu:/home/share/test# arm-linux-gnueabi-gcc --help

 -E    Preprocess only; do not compile, assemble or link

 -S    Compile only; do not assemble or link

 -c     Compile and assemble, but do not link

 -o <file>    Place the output into <file>

 


二、編譯過程

    下面只做靜態編譯的例子,爲了更加清晰的說明每個過程的變化,使用如下簡單代碼舉例

test.h:

#ifndef _TEST_H
#define _TEST_H

typedef unsigned int uint;
typedef unsigned char uchar;

static int sum(int a,int b);

#endif
test.c

#include "test.h"
#include "init.h"

int a = 4;
int b = 9;
int c;
int d;

int main(void)
{
    init(c,d);
    c = sum(a,b);
    d = e + f;

    return 0;
}

static int sum(int a,int b)
{
    return a+b;
}
inti.h

#ifndef _INIT_H
#define _INIT_H

#ifndef A

#define A 0

#endif
#define B 0

extern int e;
extern int f;

extern void init(int a,int b);

#endif
init.c

#include "init.h"

int e = 4;
int f = 6;

void init(int a,int b)
{
   a = A;
   b = B;
}

1. 預處理(Preprocessing

    預處理主要是對#開頭的關鍵字進行處理,例如#include#define、和#ifdef等等。輸入預處理指令arm-linux-gnueabi-gcc -E -P xxx.c -o xxx.i,其中-P爲去掉 行號 文件等信息,這樣更方便查看;

 

root@ubuntu:/test# arm-linux-gnueabi-gcc -E -P test.c -o test.i

test.i:

typedef unsigned int uint;  //將test.h在此展開      

typedef unsigned char uchar;

static int sum(int a,int b);

extern int e;    //將init.h在此展開                       
extern int f;
extern void init(int a,int b);

int a = 4;
int b = 9;
int c;
int d;

int main(void)
{
    init(c,d);
    c = sum(a,b);
    d = e + f;

    return 0;
}

static int sum(int a,int b)
{
    return a+b;
}

root@ubuntu:/test# arm-linux-gnueabi-gcc -E -P init.c -o init.i

Init.i:

extern int e;            //將init.h在此展開
extern int f;
extern void init(int a,int b);

int e = 4;
int f = 6;

void init(int a,int b)
{
    a = 0;              //將A的值進行替換
    b = 0;              //將B的值進行替換
}


    通過上面的例子看到,#ifdef#define都被進行了替換,不存在了,然後將#include的文件直接展開到了test.cinit.c,這也是爲什麼一般不在xxx.h中定義全局函數和變量,當有多個文件包含此頭文件時就會產生重複定義的錯誤。綜上,預處理只是對#開頭的行進行處理,其實也並不進行語法錯誤檢查(可以將變量定義改錯試試)。

2. 編譯(Compilation)

    編譯就是把C文件編程彙編,編譯的過程中要對C代碼進行語法檢查,像少了分號;或者單詞拼寫錯誤等都會造成編譯錯誤。每個工程都有很多的.c文件組成,編譯過程是對每個文件單獨進行的,對於引用的其它文件定義的全局變量或者函數,其實是不知道具體在哪,也不知掉其它文件有沒有真正定義或者定義是否正確(比如第一個編譯的文件怎麼知道它調用的外部函數被定義沒?其它文件可還沒編譯呢)。調用外部變量或者函數的聲明,也只是告訴編譯器別的地方有定義,真正有沒有編譯器其實不知道。究竟有沒有定義要到鏈接(Linking)的時候才知道。下面對test.c進行編譯而不對init.c進行編譯。

root@ubuntu:/test# gcc -S test.i

test.c

main:

@ args = 0, pretend = 0, frame = 0

@ frame_needed = 1, uses_anonymous_args = 0

push	{fp, lr}

add	fp, sp, #4

ldr	r3, .L3     //c

ldr	r2, [r3]

ldr	r3, .L3+4   //d

ldr	r3, [r3]

mov	r1, r3

mov	r0, r2

bl	init

ldr	r3, .L3+8    //a

ldr	r2, [r3]

ldr	r3, .L3+12   //b

ldr	r3, [r3]

mov	r1, r3

mov	r0, r2

bl	sum

mov	r2, r0

ldr	r3, .L3

str	r2, [r3]

ldr	r3, .L3+16    //e

ldr	r2, [r3]

ldr	r3, .L3+20    //f

ldr	r3, [r3]

add	r3, r2, r3

ldr	r2, .L3+4

str	r3, [r2]

mov	r3, #0

mov	r0, r3

pop	{fp, pc}

.L4:

.align	2

.L3:

.word	c

.word	d

.word	a

.word	b

.word	e

.word	f

.size	main, .-main

.align	2

.syntax unified

.arm

.type	sum, %function

sum:

@ args = 0, pretend = 0, frame = 8

@ frame_needed = 1, uses_anonymous_args = 0

@ link register save eliminated.

str	fp, [sp, #-4]!

add	fp, sp, #0

sub	sp, sp, #12

str	r0, [fp, #-8]

str	r1, [fp, #-12]

ldr	r2, [fp, #-8]

ldr	r3, [fp, #-12]

add	r3, r2, r3

mov	r0, r3

sub	sp, fp, #0

@ sp needed

ldr	fp, [sp], #4

bx	lr

    由上面可知,對於編譯成彙編代碼後,對函數的調用,仍然是用標號(因爲還不知道最終地址),如bl initbl sum,對變量的引用使用.L3 + offset 實現,其它架構可能有所不同,如x86仍然是直接使用abcdef

root@ubuntu:/test# gcc -S test.c -o test.s

main:

.LFB0:

.cfi_startproc

pushq	%rbp

.cfi_def_cfa_offset 16

.cfi_offset 6, -16

movq	%rsp, %rbp

.cfi_def_cfa_register 6

movl	d(%rip), %edx

movl	c(%rip), %eax

movl	%edx, %esi

movl	%eax, %edi

call	init

movl	b(%rip), %edx

movl	a(%rip), %eax

movl	%edx, %esi

movl	%eax, %edi

call	sum

movl	%eax, c(%rip)

movl	e(%rip), %edx

movl	f(%rip), %eax

addl	%edx, %eax

movl	%eax, d(%rip)

movl	$0, %eax

popq	%rbp

.cfi_def_cfa 7, 8

Ret

 

    所以,編譯過程只對單個文件進行語法解析,引用的外部變量或者函數只要使用前聲明即可編譯通過(相當於告訴編譯器別的地方有定義),提示未定義的error是鏈接過程錯誤,像鏈接之前的test.itest.stest.o都是可以生成的。

3. 彙編(Assembly)

    彙編就是將第2步編譯出來的彙編代碼(test.s轉換成機器碼。前面無論是C還是彙編代碼,都是給人看的,真正在CPU執行的只有01組成的機器碼。彙編生成的.o文件爲ELF文件

root@ubuntu:/test# arm-linux-gnueabi-gcc -c test.s

root@ubuntu:/test# file test.o

test.o: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), not stripped

查看生成的test.o

root@ubuntu:/test# arm-linux-gnueabi-readelf -S test.o

There are 11 section headers, starting at offset 0x368:

Section Headers:

  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al

  [ 0]                   NULL            00000000 000000 000000 00      0   0  0

  [ 1] .text               PROGBITS        00000000 000034 0000bc 00  AX  0   0  4

  [ 2] .rel.text            REL             00000000 0002d4 000038 08   I  9   1  4

  [ 3] .data              PROGBITS        00000000 0000f0 000008 00  WA  0   0  4

  [ 4] .bss               NOBITS          00000000 0000f8 000000 00  WA  0   0  1

  [ 5] .comment          PROGBITS        00000000 0000f8 00003c 01  MS  0   0  1

  [ 6] .note.GNU-stack     PROGBITS        00000000 000134 000000 00      0   0  1

  [ 7] .ARM.attributes    ARM_ATTRIBUTES  00000000 000134 00002a 00      0   0  1

  [ 8] .shstrtab           STRTAB          00000000 00030c 000059 00      0   0  1

  [ 9] .symtab           SYMTAB          00000000 000160 000150 10     10  13  4

  [10] .strtab            STRTAB          00000000 0002b0 000022 00      0   0  1

Key to Flags:

  W (write), A (alloc), X (execute), M (merge), S (strings)

  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)

  O (extra OS processing required) o (OS specific), p (processor specific)

從上面的信息看出,test.o基地址從 00 00 00 00開始,包含的11section,介紹其中四個;

.text 代碼段,也就是存放的是指令,函數編譯生成代碼就放在text段中

.rel.text 重定位信息,相當於告訴連接器哪些地方需要重定位

.data 定義的被初始化的全局變變量放在data段中

.bss 定義的未初始化全局變變量放在.bss段中

Textdatabss段將在最後生成的test.map中查看,這裏重點查看rel.text

root@ubuntu:/home/share/test# arm-linux-gnueabi-readelf -r test.o 

Relocation section '.rel.text' at offset 0x2d4 contains 7 entries:

 Offset     Info    Type            Sym.Value  Sym. Name

00000020  0000121c R_ARM_CALL        00000000   init

00000074  00000f02 R_ARM_ABS32       00000004   c

00000078  00001002 R_ARM_ABS32       00000004   d

0000007c  00000d02 R_ARM_ABS32       00000000   a

00000080  00000e02 R_ARM_ABS32       00000004   b

00000084  00001302 R_ARM_ABS32       00000000   e

00000088  00001402 R_ARM_ABS32       00000000   f

    test.c是單獨編譯成test.o的,因爲還不知道將來會分配到什麼地址上去,就先從00 00 00 00地址開始排布各個段(ARM架構),所以目前的地址都是臨時的。rel.text section中的信息,就相當於做了一個標記,告訴鏈接器將來這點位置的引用是要替換的。我們反彙編test.o

root@ubuntu:/home/share/test# arm-linux-gnueabi-objdump -d test.o

test.o:     file format elf32-littlearm 

Disassembly of section .text:

00000000 <main>:

   0:   e92d4800        push    {fp, lr}

   4:   e28db004        add     fp, sp, #4

   8:   e59f3064        ldr     r3, [pc, #100]  ; 74 <main+0x74>    // c

   c:   e5932000        ldr     r2, [r3]

  10:   e59f3060        ldr     r3, [pc, #96]   ; 78 <main+0x78>    //d

  14:   e5933000        ldr     r3, [r3]

  18:   e1a01003        mov     r1, r3

  1c:   e1a00002        mov     r0, r2

  20:   ebfffffe        bl      0 <init>

  24:   e59f3050        ldr     r3, [pc, #80]   ; 7c <main+0x7c>    //a

  28:   e5932000        ldr     r2, [r3]

  2c:   e59f304c        ldr     r3, [pc, #76]   ; 80 <main+0x80>    //b

  30:   e5933000        ldr     r3, [r3]

  34:   e1a01003        mov     r1, r3

  38:   e1a00002        mov     r0, r2

  3c:   eb000012        bl      8c <sum>

  40:   e1a02000        mov     r2, r0

  44:   e59f3028        ldr     r3, [pc, #40]   ; 74 <main+0x74>    //c

  48:   e5832000        str     r2, [r3]

  4c:   e59f3030        ldr     r3, [pc, #48]   ; 84 <main+0x84>    //e

  50:   e5932000        ldr     r2, [r3]

  54:   e59f302c        ldr     r3, [pc, #44]   ; 88 <main+0x88>    //f

  58:   e5933000        ldr     r3, [r3]

  5c:   e0823003        add     r3, r2, r3

  60:   e59f2010        ldr     r2, [pc, #16]   ; 78 <main+0x78>    //d

  64:   e5823000        str     r3, [r2]

  68:   e3a03000        mov     r3, #0

  6c:   e1a00003        mov     r0, r3

  70:   e8bd8800        pop     {fp, pc}

        ...

 

0000008c <sum>:

  8c:   e52db004        push    {fp}            ; (str fp, [sp, #-4]!)

  90:   e28db000        add     fp, sp, #0

  94:   e24dd00c        sub     sp, sp, #12

  98:   e50b0008        str     r0, [fp, #-8]

  9c:   e50b100c        str     r1, [fp, #-12]

  a0:   e51b2008        ldr     r2, [fp, #-8]

  a4:   e51b300c        ldr     r3, [fp, #-12]

  a8:   e0823003        add     r3, r2, r3

  ac:   e1a00003        mov     r0, r3

  b0:   e24bd000        sub     sp, fp, #0

  b4:   e49db004        pop     {fp}            ; (ldr fp, [sp], #4)

  b8:   e12fff1e        bx      lr

4. 鏈接(Linking)

    鏈接就是要把前面階段生成的xxx.o給連起來做一定的處理。在此推薦一本不錯的書《linker and loader》,裏面對鏈接和加載過程進行詳細的講解。這裏只進行大致的介紹。

root@ubuntu:/test# arm-linux-gnueabi-ld -e main test.o init.o -o test

root@ubuntu:/home/share/test# arm-linux-gnueabi-readelf -S test

There are 9 section headers, starting at offset 0x484:

Section Headers:

  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al

  [ 0]                   NULL            00000000 000000 000000 00      0   0  0

  [ 1] .text             PROGBITS          00010094 000094 0000f0 00  AX  0   0  4

  [ 2] .data             PROGBITS         00020184 000184 000010 00  WA  0   0  4

  [ 3] .bss              NOBITS           00020194 000194 000008 00  WA  0   0  4

  [ 4] .comment          PROGBITS        00000000 000194 00003b 01  MS  0   0  1

  [ 5] .ARM.attributes     ARM_ATTRIBUTES  00000000 0001cf 00002a 00      0   0  1

  [ 6] .shstrtab           STRTAB          00000000 00043f 000045 00      0   0  1

  [ 7] .symtab           SYMTAB          00000000 0001fc 0001e0 10      8  15  4

  [ 8] .strtab             STRTAB          00000000 0003dc 000063 00      0   0  1

Key to Flags:

  W (write), A (alloc), X (execute), M (merge), S (strings)

  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)

  O (extra OS processing required) o (OS specific), p (processor specific) 

這個時候我們發現可執行文件test也只有一個textdatabss session,事實上是由test.oinit.otextdatabss session合併,

 

    同時,發現rel.text沒有了,這事因爲鏈接過程中進行了重定位(relocation)。通俗的講重定位就是進行指令和數據進行轉換的過程,轉換後將使得運行時能訪問正確的指令和數據。反彙編test

00010094 <main>:

   10094:       e92d4800        push    {fp, lr}

   10098:       e28db004        add     fp, sp, #4

   1009c:       e59f3064        ldr     r3, [pc, #100]  ; 10108 <main+0x74>

   100a0:       e5932000        ldr     r2, [r3]

   100a4:       e59f3060        ldr     r3, [pc, #96]   ; 1010c <main+0x78>

   100a8:       e5933000        ldr     r3, [r3]

   100ac:       e1a01003        mov     r1, r3

   100b0:       e1a00002        mov     r0, r2

   100b4:       eb000025        bl      10150 <init>

   100b8:       e59f3050        ldr     r3, [pc, #80]   ; 10110 <main+0x7c>

   100bc:       e5932000        ldr     r2, [r3]

   100c0:       e59f304c        ldr     r3, [pc, #76]   ; 10114 <main+0x80>

   100c4:       e5933000        ldr     r3, [r3]

   100c8:       e1a01003        mov     r1, r3

   100cc:       e1a00002        mov     r0, r2

   100d0:       eb000012        bl      10120 <sum>

   100d4:       e1a02000        mov     r2, r0

   100d8:       e59f3028        ldr     r3, [pc, #40]   ; 10108 <main+0x74>

   100dc:       e5832000        str     r2, [r3]

   100e0:       e59f3030        ldr     r3, [pc, #48]   ; 10118 <main+0x84>

   100e4:       e5932000        ldr     r2, [r3]

   100e8:       e59f302c        ldr     r3, [pc, #44]   ; 1011c <main+0x88>

   100ec:       e5933000        ldr     r3, [r3]

   100f0:       e0823003        add     r3, r2, r3

   100f4:       e59f2010        ldr     r2, [pc, #16]   ; 1010c <main+0x78>

   100f8:       e5823000        str     r3, [r2]

   100fc:       e3a03000        mov     r3, #0

   10100:       e1a00003        mov     r0, r3

   10104:       e8bd8800        pop     {fp, pc}

   10108:       00020194        .word   0x00020194

   1010c:       00020198        .word   0x00020198

   10110:       00020184        .word   0x00020184

   10114:       00020188        .word   0x00020188

   10118:       0002018c        .word   0x0002018c

   1011c:       00020190        .word   0x00020190

 

00010120 <sum>:

   10120:       e52db004        push    {fp}            ; (str fp, [sp, #-4]!)

   10124:       e28db000        add     fp, sp, #0

   10128:       e24dd00c        sub     sp, sp, #12

   1012c:       e50b0008        str     r0, [fp, #-8]

   10130:       e50b100c        str     r1, [fp, #-12]

   10134:       e51b2008        ldr     r2, [fp, #-8]

   10138:       e51b300c        ldr     r3, [fp, #-12]

   1013c:       e0823003        add     r3, r2, r3

   10140:       e1a00003        mov     r0, r3

   10144:       e24bd000        sub     sp, fp, #0

   10148:       e49db004        pop     {fp}            ; (ldr fp, [sp], #4)

   1014c:       e12fff1e        bx      lr

 

00010150 <init>:

   10150:       e52db004        push    {fp}            ; (str fp, [sp, #-4]!)

   10154:       e28db000        add     fp, sp, #0

   10158:       e24dd00c        sub     sp, sp, #12

   1015c:       e50b0008        str     r0, [fp, #-8]

   10160:       e50b100c        str     r1, [fp, #-12]

   10164:       e3a03000        mov     r3, #0

   10168:       e50b3008        str     r3, [fp, #-8]

   1016c:       e3a03000        mov     r3, #0

   10170:       e50b300c        str     r3, [fp, #-12]

   10174:       e1a00000        nop                     ; (mov r0, r0)

   10178:       e24bd000        sub     sp, fp, #0

   1017c:       e49db004        pop     {fp}            ; (ldr fp, [sp], #4)

   10180:       e12fff1e        bx      lr

通過重定位,高亮部分已經被替換成的正確的地址。重定位會發生在三個時刻:

1程序編譯鏈接接時

2、程序裝入內存時

3、程序執行時

像編譯的test文件,就是在鏈接時完成地址轉換。像程序裝入時和程序執行時的重定位,可查看《linker and loader》。

三、結語

    整個編譯過程其實是很複雜,上面只是簡單說明了主要部分,像debug信息、符號表和鏈接器的重定位過程等很多部分並沒有涉及,想要精通的話,還是要學習編譯原理等專業書籍。

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章