嵌入式Linux编程基础知识

源文件需要经过编译才能生成可执行文件。在windows下进行开发时,只需要单击几个按钮即可编译,继承开发环境已经将各种编译工具的使用封装好了。linux下也有很多优秀的的集成开发工具,但是更多的时候是直接使用编译工具:即使使用集成开发工具,也需要掌握一些编译选项。

PC上的编译工具链为fcc、ld、objcopy、objdump等,它们编译出来的程序在x86平台上运行。要编译出能在ARM平台上运行的程序,必须使用交叉编译工具arm-linux-gcc、arm-linux-ld等。

一、arm-linux-gcc

一个c/c++文件要经过预处理、编译、汇编和链接等4步才能编程可执行文件。

  • 预处理:c/c+++源文件中,以#开头的命令统称为预处理命令,如包含命令#include、宏定义命令#define、条件编译命令#if、#ifdef等。预处理就是将要包含(include)的文件插入到源文件、将宏定义展开、根据条件编译命令选择要使用的代码,最后讲这些代码输出到一个.i文件中等待进一步处理。预处理将用到arm-linux-cpp工具。
  • 编译:编译就是把c/c++代码(比如上述的.i文件)翻译成汇编代码,所用到的工具为ccl。
  • 汇编:汇编就是将第二部输出的汇编代码翻译成符合一定格式的机器代码,在linux系统上一般表现为ELF目标文件(OBJ文件),用到的工具为arm-linux-as。反汇编是将机器代码转为汇编代码,这在调试程序时常常用到。
  • 链接:链接就是将上步上城的OBJ文件和系统库的OBJ文件、库文件链接起来,最终生成可以在特定平台运行的可执行文件,用到的工具为arm-linux-ld。

编译器利用这四个步骤中的一个或多个来处理输入文件,源文件的后缀名表示源文件所用的语言,后缀名控制着编译器的默认动作,如下表所示:

后缀名 语言种类 后期操作
.c c源程序 预处理、编译、汇编
.C c++源程序 预处理、编译、汇编
.cc c++源程序 预处理、编译、汇编
.cxx c++源程序 预处理、编译、汇编
.m objective-c源程序 预处理、编译、汇编
.i 预处理后的c文件 编译、汇编
.ii 预处理后的c++文件 编译、汇编
.s 汇编语言源程序 汇编
.S 汇编语言源程序 预处理、汇编
.h 预处理器文件

通常不出现在命令行上

其他后缀名的文件被传递给链接器,通常包括以下两种:

  • .o:目标文件(OBJ文件)
  • .a:归档库文件

在编译过程中,除非使用了-c、-S或-E选项,否则最后的步骤总是链接。在链接阶段、所有对应于源程序的.o文件、-l选项指定的库文件、无法识别的文件名(包括指定的.o目标文件和.a库文件)按命令行中的顺序传递给链接器。

以一个简单的"Hello,world!" c程序为例,在/work/hardware目录下创建hello.c文件,代码如下:

#include <stdio.h>
int main(int argc,char *argv[])
{
  printf("Hello World!\n");
  return 0;
}

使用arm-linux-gcc,只需要一个命令就可以生成可执行文件hello,它包含了以上4个步骤:

arm-linux-gcc -o hello hello.c

如果想查看编译的细节,加上-v选项:

arm-linux-gcc -v -o hello hello.c

下面我们介绍一下arm-linux-gcc一些常用的选项。

1.1 总体选项

  • -c  :只预处理、编译和汇编源程序,不进行链接。编译器对每一个源程序产生一个目标文件。
  • -S : 编译后即停止,不进行汇编,对于每个输入的非汇编语言文件,输出结果是汇编语言文件。
  • -E : 预处理后即停止,不进行编译。
  • -o  file: 确定输出文件为file。如果没有用-o选项,缺省的可执行文件的输出是a.out,目标文件和汇编文件的输出对source.suffix分别是source.o和source.s,预处理的C源程序的输出是标准输出stdout。
  • -v : 显示具体执行的命令信息。

在/work/hardware/options目录下,新建如下源文件:

main.c:

#include <stdio.h>
#include "sub.h"

int main(int argc, char *argv[])
{
   int i;
   printf("Min fun!\n");
   sub_fun();
   return 0;
}

sub.h:

void sub_fun();

sub.c:

#include <stdio.h>
void
sub_fun() { printf("Sub fun!\n"); }

arm-linux-gcc、arm-linux-ld等工具与gcc、ld等工具的使用方法相似,很多选项是一样的,主要区别是一个编译出来的程序是运行在ARM上,一个是运行在PC机上。这里为了演示这些命令的效果,使用gcc、ld等工具进行编译链接,使用上面介绍的选项进行编译,命令如下:

gcc -c -o main.o main.c
gcc -c -o sub.o sub.c
gcc  -o test main.o sub.o

嵌入式Linux编程基础知识

其中main.o、sub.o是经过了预处理、编译、汇编后生成的OBJ文件,它们还没有被链接成可执行文件:最后一步将它们连接处可执行test,可以直接运行一下命令:

./test

嵌入式Linux编程基础知识

現在试试其他选项,一下命令生成的main.s是main.c的汇编语言文件:

gcc -S -o main.s main.c

嵌入式Linux编程基础知识

以下命令对main.c进行预处理,并将得到的结果打印出来。里面扩展了所有包含的文件、所有定义的宏,在编写程序时,有时候查找某个宏定义是非常繁琐的事,可以使用-dM-E选项来查看,命令如下:

gcc -E main.c

1.2 警告选项

 - Wall 选项基本打开所有需要注意的警告信息,比如没有指定类型的声明、在声明之前就使用函数、局部变量除了声明就没再使用等。

1.3 调试选项

-g  :  产生一张用于调试和排错的扩展符号表。-g选项使程序可以用GNU的调试程序GDB进行调试。优化和调试通常不兼容,同时使用-g和-O(-O2)选项经常会使程序产生奇怪的运行结果。所以不要同时使用-g和-O(-O2)选项

1.4 优化选项

 - O或-O1: 对于大函数、优化编译的过程将占用较长时间和相当大的内存。不使用-O选项的目的是减少编译的开销,使编译结果能够调试、语句是独立的。不使用-O或-O1选项时,只有声明了register的变量才分配使用寄存器。使用了-O或-O1选项时,编译器会师徒减少目标码的大小和执行时间。

-O2: 多优化一些,除了涉及空间和速度交换的优化选项,执行几乎所有的优化工作。例如不进行循环展开(loop unrolling)和函数内嵌(inlining)。和-O选项相比,这个选项既增加了编译时间,也提高了生成代码的运行效果。在一般应用中,经常使用该选项。

-O3: 优化的更多,除了打开-O所做的一切,它还打开了-finline-function选项。

-O0: 不优化。

如果指定了多个-O选项,不管带不带数字、生效的是最后一个选项。

1.5 链接器选项

-lname  :在连接时使用函数库libname.a,连接程序在-Ldir选项指定的目录下和/lib,/usr/lib目录下寻找该库文件。在没有使用-static选项时,如果发现共享函数库libname.so,则使用libname.so进行动态连接。
-static : 禁止与共享函数库连接。
-shared :尽量与共享函数库连接。

二、arm-linux-ld

arm-linux-ld用于将多个目标文件、库文件链接成可执行文件。 

 -T : 可以直接使用它来指定代码段、数据段、bss段的起始地址,也可以用来指定一个链接脚本、在链接脚本中进行更复杂的地址设置。-T选项只用于链接Bootloader、内核等没有底层软件支持的软软件,链接运行于操作系统之上的应用程序时,无需指定-T选项,它们使用默认的方式进行链接。

2.1、直接指定代码段、数据段、bss段的起始地址

格式如下:

-Ttext  startaddr
-Tdata startaddr
-Tbss   startaddr

其中的startaddr分别表示代码段、数据段和bss段的起始地址,它是一个十六进制数,比如:

arm-linux-ld -Ttext 0x00000000 -g led_on.o -o led_on_elf

它表示代码段的运行地址为0x0000000,由于没有定义数据段、bss段的起始地址,它们被依次放在代码段的后面。

以一个例子说明-Ttext选项作用,在/work/hardware/link目录下,新建link.s文件:

.text
.global _start
_start:
        b step1
step1:
        ldr pc, =step2
step2:
        b step2

使用下面的命令编译、链接、反汇编:

arm-linux-gcc -c -o link.o link.s
arm-linux-ld -Ttext 0x00000000 link.o -o link_elf_0x00000000
arm-linux-ld -Ttext 0x30000000 link.o -o link_elf_0x30000000
arm-linux-objdump -D link_elf_0x00000000 > link_0x00000000.dis
arm-linux-objdump -D link_elf_0x30000000 > link_0x30000000.dis

link.s中用到两种跳转方法:b跳转指定、直接向pc寄存器赋值。先列出不同-Ttext选项下生成的反汇编文件,再详细分析由于不同运行地址带来的差异及影响,这两个反汇编文件如下:

嵌入式Linux编程基础知识

 嵌入式Linux编程基础知识

 先看link.s中第一条指令,b step1,b跳转指令是一个相对跳转指令、其机器码格式如下:

Cond 1 0 1 L Offset

[31:28] 位是条件码;

[27:24]位为1010时,表示b跳转指令,为1011时表示bl跳转指令;

[23:0]表示偏移地址;

使用b或bl跳转时,下一跳指令的地址是这样计算的;将指令中24位带符号的补码扩展为32位(扩展其符号位);将此32位数左移两位;将得到的值加到pc寄存器中,即得到跳转的目标地址。

第一条指令b step1的机器码为eaffffff。

(1) 24位带符号的补码为0xffffff,将它扩展为32位得到0xffffffff。

(2) 将次32位数左移两位得到0xfffffffc,其值就是-4;

(3) pc的值是当前指令的下两条指令的地址,加上步骤2得到-4,这恰好是第二条指定step1的地址。

不要被反汇编代码的b 0x4迷惑,它不是指跳到绝对地址0x4处执行,绝对地址需要按照上述3个步骤计算。可以发现,b跳转指令依赖于当前pc寄存器的值,这个特性使得b指令的程序不依赖于代码存储的位置——即不管这条代码放在什么位置,b指令都可以跳到正确的位置。这类指令被称为位置无关码,使用不同的-Ttext选项,生成的代码仍然是一样的。

再看第二条指令ldr pc.=step2.从汇编码ldr pc,[pc,#02]可以看出,这条指令从内存某个位置读出数据,并赋值给pc寄存器。这个位置的地址是当前pc寄存器的值加上便宜值0,其中存放的值依赖于链接命令的-Ttext选项,执行这条命令后,对于link_0x00000000.dis,pc=0x00000008;对于link_0x30000000.dis,pc=0x30000008。执行第三条指令b step2后,程序的运行地址就不同了,分别是0x00000008,0x30000008.

Bootloader、内核等程序刚开始执行时,他们所处的地址通常不等于运行地址。在程序开头,先试用b、bl、mov等位置无关的指令将代码从Flash等设备复制到内存的运行地址处,然后在跳到运行地址去执行。

 

 

参考文章:

【1】gcc和arm-linux-gcc

嵌入式Linux编程基础知识

上一篇:ubuntu1804时间相差8小时


下一篇:Linux下的编译调试命令