我们需要编译出运行在ARM平台上的代码,所使用的交叉编译器为arm-linux-gcc。下面将arm-linux-gcc编译工具的一些常用命令参数介绍给大家。在此之前首先介绍下编译器的工作过程,在使用GCC编译程序时,编译过程分为四个阶段:
1) 预处理(Pre-Processing)
2) 编译(Compiling)
3) 汇编(Assembling)
4) 链接(Linking)
Linux程序员可以根据自己的需要让 GCC在编译的任何阶段结束,以便检查或使用编译器在该阶段的输出信息,或者对最后生成的二进制文件进行控制,以便通过加入不同数量和种类的调试代码来为今后的调试做好准备。和其它常用的编译器一样,GCC也提供了灵活而强大的代码优化功能,利用它可以生成执行效率更高的代码。
以文件example.c为例说明它的用法
1. arm-linux-gcc -o example example.c
不加-c、-S、-E参数,编译器将执行预处理、编译、汇编、连接操作直接生成可执行代码。
-o参数用于指定输出的文件,输出文件名为example,如果不指定输出文件,则默认输出a.out
2. arm-linux-gcc -c -o example.o example.c
-c参数将对源程序example.c进行预处理、编译、汇编操作,生成example.0文件
去掉指定输出选项"-o example.o"自动输出为example.o,所以说在这里-o加不加都可以
3. arm-linux-gcc -S -o example.s example.c
-S参数将对源程序example.c进行预处理、编译,生成example.s文件
-o选项同上
4. arm-linux-gcc -E -o example.i example.c
-E参数将对源程序example.c进行预处理,生成example.i文件(不同版本不一样,有的将预处理后的内容打印到屏幕上)
就是将#include,#define等进行文件插入及宏扩展等操作。
5. arm-linux-gcc -v -o example example.c
加上-v参数,显示编译时的详细信息,编译器的版本,编译过程等。
6. arm-linux-gcc -g -o example example.c
-g选项,加入GDB能够使用的调试信息,使用GDB调试时比较方便。
7. arm-linux-gcc -Wall -o example example.c
-Wall选项打开了所有需要注意的警告信息,像在声明之前就使用的函数,声明后却没有使用的变量等。
8. arm-linux-gcc -Ox -o example example.c
-Ox使用优化选项,X的值为空、0、1、2、3
0为不优化,优化的目的是减少代码空间和提高执行效率等,但相应的编译过程时间将较长并占用较大的内存空间。
9. arm-linux-gcc -I /home/include -o example example.c
-Idirname: 将dirname所指出的目录加入到程序头文件目录列表中。如果在预设系统及当前目录中没有找到需要的文件,就到指定的dirname目录中去寻找。
10. arm-linux-gcc -L /home/lib -o example example.c
-Ldirname:将dirname所指出的目录加入到库文件的目录列表中。在默认状态下,连接程序ld在系统的预设路径中(如/usr/lib)寻找所需要的库文件,这个选项告诉连接程序,首先到-L指定的目录中去寻找,然后再到系统预设路径中寻找。
11. arm-linux-gcc –static -o libexample.a example.c
静态链接库文件
gcc在命令行上经常使用的几个选项是:
-c 只预处理、编译和汇编源程序,不进行连接。编译器对每一个源程序产生一个目标文件。
-o file 确定输出文件为file。如果没有用-o选项,缺省的可执行文件的输出是a.out,目标文件和汇编文件的输出对source.suffix分别是source.o和source.s,预处理的C源程序的输出是标准输出stdout。
-Dmacro 或-Dmacro=defn 其作用类似于源程序里的#define。例如:% gcc -c -DHAVE_GDBM -DHELP_FILE=\"help\" cdict.c其中第一个- D选项定义宏HAVE_GDBM,在程序里可以用#ifdef去检查它是否被设置。第二个-D选项将宏HELP_FILE定义为字符串“help”(由于反斜线的作用,引号实际上已成为该宏定义的一部分),这对于控制程序打开哪个文件是很有用的。
-Umacro 某些宏是被编译程序自动定义的。这些宏通常可以指定在其中进行编译的计算机系统类型的符号,用户可以在编译某程序时加上 -v选项以查看gcc缺省定义了哪些宏。如果用户想取消其中某个宏定义,用-Umacro选项,这相当于把#undef macro放在要编译的源文件的开头。
-Idir 将dir目录加到搜寻头文件的目录列表中去,并优先于在gcc缺省的搜索目录。在有多个-I选项的情况下,按命令行上-I选项的前后顺序搜索。dir可使用相对路径,如-I../inc等。
-O 对程序编译进行优化,编译程序试图减少被编译程序的长度和执行时间,但其编译速度比不做优化慢,而且要求较多的内存。
-O2 允许比-O更好的优化,编译速度较慢,但结果程序的执行速度较快。
-g 产生一张用于调试和排错的扩展符号表。-g选项使程序可以用GNU的调试程序GDB进行调试。优化和调试通常不兼容,同时使用-g和-O(-O2)选项经常会使程序产生奇怪的运行结果。所以不要同时使用-g和-O(-O2)选项。
-fpic或-fPIC 产生位置无关的目标代码,可用于构造共享函数库。
以上是gcc的编译选项。gcc的命令行上还可以使用连接选项。事实上,gcc将所有不能识别的选项传递给连接程序ld。连接程序ld将几个目标文件和库程序组合成一个可执行文件,它要解决对外部变量、外部过程、库程序等的引用。但我们永远不必要显式地调用ld。利用gcc命令去连接各个文件是很简单的,即使在命令行里没有列出库程序,gcc也能保证某些库程序以正确的次序出现。
gcc的常用连接选项有下列几个:
-Ldir 将dir目录加到搜寻-l选项指定的函数库文件的目录列表中去,并优先于gcc缺省的搜索目录。在有多个-L选项的情况下,按命令行上-L选项的前后顺序搜索。dir可使用相对路径。如-L../lib等。
-lname 在连接时使用函数库libname.a,连接程序在-Ldir选项指定的目录下和/lib,/usr/lib目录下寻找该库文件。在没有使用-static选项时,如果发现共享函数库libname.so,则使用libname.so进行动态连接。
-static 禁止与共享函数库连接。
-shared 尽量与共享函数库连接
Linux 下动态链接库(shared object file,共享对象文件)的文件后缀为.so
,它是一种特殊的目标文件(object file),可以在程序运行时被加载(链接)进来。使用动态链接库的优点是:程序的可执行文件更小,便于程序的模块化以及更新,同时,有效内存的使用效率更高。
GCC 生成动态链接库
如果想创建一个动态链接库,可以使用 GCC 的-shared
选项。输入文件可以是源文件、汇编文件或者目标文件。
另外还得结合-fPIC
选项。-fPIC 选项作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code);这样一来,产生的代码中就没有绝对地址了,全部使用相对地址,所以代码可以被加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。
例如,从源文件生成动态链接库:
$ gcc -fPIC -shared func.c -o libfunc.so
从目标文件生成动态链接库:
$ gcc -fPIC -c func.c -o func.o
$ gcc -shared func.o -o libfunc.so
-fPIC 选项作用于编译阶段,在生成目标文件时就得使用该选项,以生成位置无关的代码。
GCC 将动态链接库链接到可执行文件
如果希望将一个动态链接库链接到可执行文件,那么需要在命令行中列出动态链接库的名称,具体方式和普通的源文件、目标文件一样。请看下面的例子:
$ gcc main.c libfunc.so -o app.out
将 main.c 和 libfunc.so 一起编译成 app.out,当 app.out 运行时,会动态地加载链接库 libfunc.so。
当然,必须要确保程序在运行时可以找到这个动态链接库。你可以将链接库放到标准目录下,例如 /usr/lib,或者设置一个合适的环境变量,例如 LIBRARY_PATH。不同系统,具有不同的加载链接库的方法。
ARM函数调用过程分析
1. ARM的栈帧 先来看看ARM的栈帧布局图:
上图描述的是ARM的栈帧布局方式,main stack frame为调用函数的栈帧,func1 stack frame为当前函数(被调用者)的栈帧,栈底在高地址,栈向下增长。图中FP就是栈基址,它指向函数的栈帧起始地址;SP则是函数的栈指针,它指向栈顶的位置。ARM压栈的顺序很是规矩,依次为当前函数指针PC、返回指针LR、栈指针SP、栈基址FP、传入参数个数及指针、本地变量和临时变量。如果函数准备调用另一个函数,跳转之前临时变量区先要保存另一个函数的参数。
ARM也可以用栈基址和栈指针明确标示栈帧的位置,栈指针SP一直移动,相比于x86,ARM更为鲜明的特点是,两个栈空间内的地址(SP+FP)前面,必然有两个代码地址(PC+LR)明确标示着调用函数位置内的某个地址。
2. ARM的汇编指令和栈操作 ARM微处理器共有37个寄存器,其中31个为通用寄存器,6个为状态寄存器。但是这些寄存器不能被同时访问,具体哪些寄存器是可编程访问的,取决于微处理器的工作状态及具体的运行模式。但在任何时候,通用寄存器R0~R15、一个或两个状态寄存器都是可访问的。有三个特殊的通用寄存器:
寄存器R13:在ARM指令中常用作堆栈指针SP
寄存器R14:也称作子程序连接寄存器(Subroutine Link Register)即连接寄存器LR
寄存器R15:也称作程序计数器PC
ARM进行函数内压栈和出栈往往使用如下的语句:
stmfd sp!, {r0-r9, lr} ; 满递减入栈,给寄存器r0-r9,lr压栈,sp不断减4
ldmfd sp!, {r0-r9, pc} ; 满递减出栈,给寄存器r0-r9出栈,并使程序跳转回函数的调用点,sp不断增4
常用的函数内外跳转指令有mov和BL,ARM有两种跳转方式:
(1)mov pc, <跳转地址〉
这种向程序计数器PC直接写跳转地址,能在4GB连续空间内任意跳转。
(2)通过 B BL BLX BX 可以完成在当前指令向前或者向后32MB的地址空间的跳转(为什么是32MB呢?寄存器是32位的,此时的值是24位有符号数,所以32MB?后面再查查看)。B是最简单的跳转指令。要注意的是,跳转指令的实际值不是绝对地址,而是相对地址——是相对当前PC值的一个偏移量,它的值由汇编器计算得出。BL很常用,它在跳转之前会在寄存器LR(R14)中保存PC的当前内容。BL的经典用法如下:
bl NEXT ; 跳转到NEXT
……
NEXT
……
mov pc, lr ; 从子程序返回。
看代码:
int func(int a, int b, int c, int d)
{
return 1;
}
int main()
{
int i = 1, j = 2;
func(i, j, 3, 4);
return 0;
}
使用arm-linux-gcc编译后,使用ida打开:
.text:000083D0 EXPORT main
.text:000083D0 main ; DATA XREF: .text:000082C4o
.text:000083D0 ; .text:off_82DCo
.text:000083D0
.text:000083D0 b = -0x14
.text:000083D0 a = -0x10
.text:000083D0
.text:000083D0 IP = R12
.text:000083D0 FP = R11
.text:000083D0 MOV IP, SP
.text:000083D4 STMFD SP!, {FP,IP,LR,PC}
.text:000083D8 SUB FP, IP, #4
.text:000083DC SUB SP, SP, #8
.text:000083E0 MOV R3, #1
.text:000083E4 STR R3, [FP,#a]
.text:000083E8 MOV R3, #2
.text:000083EC STR R3, [FP,#b]
.text:000083F0 LDR R0, [FP,#a]
.text:000083F4 LDR R1, [FP,#b]
.text:000083F8 MOV R2, #3
.text:000083FC MOV R3, #4
.text:00008400 BL func
.text:00008404 MOV R3, #0
.text:00008408 MOV R0, R3
.text:0000840C SUB SP, FP, #0xC
.text:00008410 LDMFD SP, {FP,SP,PC}
.text:00008410 ; End of function main
可以发现,在main函数中,使用IP(R12)暂时保存栈指针sp,然后使用堆栈操作指令stmfd将栈帧(FP)、IP、程序返回地址(LR)、程序计数器(PC)压栈,以保护现场,然后使用sub fp,ip,#4使fp指向当前函数栈帧的栈底,sub sp,sp,#8,为当前函数局部变量分配看空间。接下来通过寄存器传递参数r1,r2,r3,r4。使用BL指令调用函数,BL指令同时也会将当前指令的下一条指令地址赋给LR,以跳转回来。最后使用ldmfd恢复现场。
.text:000083A0 ; =============== S U B R O U T I N E =======================================
.text:000083A0
.text:000083A0 ; Attributes: bp-based frame
.text:000083A0
.text:000083A0 EXPORT func
.text:000083A0 func ; CODE XREF: main+30p
.text:000083A0
.text:000083A0 var_1C = -0x1C
.text:000083A0 var_18 = -0x18
.text:000083A0 var_14 = -0x14
.text:000083A0 var_10 = -0x10
.text:000083A0
.text:000083A0 MOV R12, SP
.text:000083A4 STMFD SP!, {R11,R12,LR,PC}
.text:000083A8 SUB R11, R12, #4
.text:000083AC SUB SP, SP, #0x10
.text:000083B0 STR R0, [R11,#var_10]
.text:000083B4 STR R1, [R11,#var_14]
.text:000083B8 STR R2, [R11,#var_18]
.text:000083BC STR R3, [R11,#var_1C]
.text:000083C0 MOV R3, #1
.text:000083C4 MOV R0, R3
.text:000083C8 SUB SP, R11, #0xC
.text:000083CC LDMFD SP, {R11,SP,PC}
.text:000083CC ; End of function func
.text:000083CC
.text:000083D0
.text:000083D0 ; =============== S U B R O U T I N E =======================================