嵌入式软件开发之------浅谈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信息、符号表和链接器的重定位过程等很多部分并没有涉及,想要精通的话,还是要学习编译原理等专业书籍。

 

 

 

 

 

  • 5
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值