一、目的
本文分析ARMCC编译器编译出来的文件对应的汇编代码,旨在帮助C语言程序员写嵌入式代码时分析堆栈使用问题和优化C语言代码。
二、材料
1、工具
编译器:Keil5 ARMCC
反编译工具:fromelf.exe
2、c语言源码
struct LG_BaseLockFuncs_t;
struct DynMem_Init_t
{
void * pool; //point to start address of dynamic memory
size_t size; //size of dynamic memory
int nMemAlign; //memory align
const struct LG_BaseLockFuncs_t *pLockCBs; //Callback functions to lock the heap for multi-threads.
void *pLock; //parameter which will be passed to 'pLockCBs'.
};
extern uint8_t ImageRW_IRAM1ZILimit[];
static void meminit(void)
{
struct DynMem_Init_t dmInit;
static uint32_t irq_flag;
static const struct LG_BaseLockFuncs_t irq_lockcbs = {IRQ_Disable, IRQ_Enable};
dmInit.nMemAlign = 8;
dmInit.pool = ImageRW_IRAM1ZILimit;
dmInit.size = 0x20000000 + 0x5000 - (int)dmInit.pool;
dmInit.pLock = &irq_flag;
dmInit.pLockCBs = &irq_lockcbs;
DynMem_Init(&dmInit);
}
3、步骤
3.1 用keil对源码进行编译,在输出目录下生成:<输出文件名>.axf
3.2 在keil安装目录下,运行命令行:l\ARM\ARMCC\bin>fromelf.exe -c <输出文件路径>.axf > a.txt
3.3 打开txt,找到要分析的函数名
三、分析汇编伪代码
1、汇编代码如下所示:
i.meminit
meminit
0x08008214: b500 .. PUSH {lr}
0x08008216: b085 .. SUB sp,sp,#0x14
0x08008218: 2008 . MOVS r0,#8
0x0800821a: 9002 .. STR r0,[sp,#8]
0x0800821c: 4807 .H LDR r0,[pc,#28] ; [0x800823c] = 0x20001220
0x0800821e: 9000 .. STR r0,[sp,#0]
0x08008220: 4907 .I LDR r1,[pc,#28] ; [0x8008240] = 0x20005000
0x08008222: 9800 .. LDR r0,[sp,#0]
0x08008224: 1a08 .. SUBS r0,r1,r0
0x08008226: 9001 .. STR r0,[sp,#4]
0x08008228: 4806 .H LDR r0,[pc,#24] ; [0x8008244] = 0x20000018
0x0800822a: 9004 .. STR r0,[sp,#0x10]
0x0800822c: 4806 .H LDR r0,[pc,#24] ; [0x8008248] = 0x8008bb4
0x0800822e: 9003 .. STR r0,[sp,#0xc]
0x08008230: 4668 hF MOV r0,sp
0x08008232: f7fafc6b ..k. BL DynMem_Init ; 0x8002b0c
0x08008236: b005 .. ADD sp,sp,#0x14
0x08008238: bd00 .. POP {pc}
$d
0x0800823a: 0000 .. DCW 0
0x0800823c: 20001220 .. DCD 536875552
0x08008240: 20005000 .P. DCD 536891392
0x08008244: 20000018 ... DCD 536870936
0x08008248: 08008bb4 .... DCD 134253492
2、分析寄存器的使用
观察汇编代码符号meminit到符号$d部分,可见用到的寄存器有r0/r1/sp/lr/pc,sp是用来存储堆栈地址的,pc是用来记录下一条要运行的指令的地址的。程序中有一条跳转指令BL,是用来调用其他函数的。有调用其他函数,被调用的函数里面怎么操作寄存器,调用者是不知道的,有可能会修改寄存器的值,所以,需要再调用前保存寄存器的值,调用后再恢复。这个保存和恢复现场操作,有两种实现方式:
a. 调用者保存和恢复用到的寄存器;
b. 被调用者保存和恢复用到的寄存器;
无论是a或b,都不需要保存全部寄存器的值,因为调用者和被调用的函数都知道自己会修改哪些寄存器,保存自己修改的寄存器的值即可,并不需要知道对方用到什么寄存器。
现在看meminit保存和恢复了哪些寄存器的值,观察函数开始出地址0x08008214和0x08008216、结束处0x08008236和0x08008238处语句,保存和恢复的是sp和lr,其中0x08008238 POP {pc}等效于POP {lr} 和 MOV PC, LR。PC是控制跳转的,最后一句是跳转回调用meminit处。所以,保存和恢复的是sp/lr/pc,r0/r1没有被保存和恢复,由此可推断出,ARMCC编译C代码的时候,以跳转语句为分界线,r0/r1保存的是临时值,是不需要保存和恢复的。
另外,meminit保存和恢复寄存器的值出现在函数开始处和结束处,并不是BL的前后,可以推断出,ARMCC保存和恢复现场使用的是方式b。
那么,具体哪些寄存器是不需要保存和恢复的呢?我们知道,调用函数时,参数中前4个整数空间的值的直接放在r0~r3(多出部分放在调用函数的栈底),可以猜测,r0~r3是就是这些不需要保存和恢复的,让我们来看另一段c代码:
struct MemMgr_t mgr;
void DynMem_Init(const struct DynMem_Init_t * pInitCfg)
{
MemMgr_Init(&mgr, pInitCfg);
}
DynMem_Init
0x08002b0c: b510 .. PUSH {r4,lr}
0x08002b0e: 4604 .F MOV r4,r0
0x08002b10: 4621 !F MOV r1,r4
0x08002b12: 4802 .H LDR r0,[pc,#8] ; [0x8002b1c] = 0x200009b4
0x08002b14: f004fd80 .... BL MemMgr_Init ; 0x8007618
0x08002b18: bd10 .. POP {r4,pc}
$d
0x08002b1a: 0000 .. DCW 0
0x08002b1c: 200009b4 ... DCD 536873396
可以看到,函数在BL前后,保存和恢复了r4,由此可以验证我们的推测是正确的。
3、ARMCC编译出来的存储空间
回到meminit的C函数,该函数定义了一个局部变量struct DynMem_Init_t dmInit;和两个静态变量static uint32_t irq_flag;
static const struct LG_BaseLockFuncs_t irq_lockcbs;
其中sizeof(dmInit)=sizeof(struct DynMem_Init_t)=sizeof(void *)+sizeof(size_t)+sizeof(int)+sizeof(const struct LG_BaseLockFuncs_t *)+sizeof(void *)=20bytes,sizeof(irq_flag)=sizeof(uint32_t)=4bytes,sizeof(irq_lockcbs)=sizeof(struct LG_BaseLockFuncs_t)=8bytes,而汇编地址为0x08008216的语句将堆栈指针自减了0x14=20bytes,在此可以猜测,堆栈里仅保存了局部变量dmInit。查看C语言代码,发现整个函数的使用到的左操作数均是变量dmInit的成员变量,亦即修改的均是dmInit的成员变量;查看汇编代码STR语句,会发现,所有的写内存操作的目的地址都是通过堆栈指针sp加上一个偏移来进行的。综上所述,保存完lr后,堆栈里的确只保存了一个局部变量dmInit。那么,静态变量是怎么访问的呢?看最后一条写内存的语句dmInit.pLockCBs = &irq_lockcbs对应的汇编语句:
0x0800822c: 4806 .H LDR r0,[pc,#24] ; [0x8008248] = 0x8008bb4
0x0800822e: 9003 .. STR r0,[sp,#0xc]
它读的是地址为[pc+24]处存储的一个整数,也就是说,&irq_lockcbs即静态变量irq_lockcbs的地址是存放在代码段附近的数据段的,具体位置是0x0800822c + 8 + 24(8是由于CM3采用的是取址、解码、执行的3级流水线,PC存储的是地址亦即当前执行指令的后面2条指令的地址)。也就是说,静态变量的地址是跟代码段放在一块的(数据段),代码段应该是放在flash里面的,静态变量的地址是一个常量,也就是说,静态变量的地址是编译链接时就固定好了的,堆栈是会变的,那么静态变量它肯定是放在堆栈外的。
既然静态变量的地址是编译链接时固定好了的,那么,它存放在哪里呢?我们看它的地址值[0x8008248] = 0x8008bb4,发现,它的值也是在数据段的,数据段是不可直接写的,那么静态变量怎会放在数据段呢?我们该静态变量的定义:
static const struct LG_BaseLockFuncs_t irq_lockcbs = {IRQ_Disable, IRQ_Enable};
它是一个常量,这就可以解释得通了,常量不用修改,是可以放在数据段里面的。这也解释了强行将常量地址改成非常量指针后,对指针存储的东西进行修改为什么会不成功。
然后,我们看倒数第二个修改语句,它读的地址是非常量的静态变量irq_flag的:
0x08008228: 4806 .H LDR r0,[pc,#24] ; [0x8008244] = 0x20000018
这回地址0x20000018终于在内存里了,并且该值离内存地址起点0x20000000很近,表明静态变量的是存放在内存地址起始处附近的。
4、中断
分析了普通执行流寄存器的使用,r0~r3在调用函数时是无需保存到堆栈里面去的,它们在跳转语句前后的值并不关联,它们的值的维持是以跳转语句为分界线的。但是,如果发生中断,它们的值会不会被中断例程修改,中断返回后,而产生错误的结果呢?如果是的话,那么编译器就需要区分中断例程和普通函数了,在中断例程里先对r0~r3压栈,退出前出栈。但是,查看汇编代码,并没有做这样的处理,原因参考相应的内核手册,如《Cortex-M3权威指南》提到:
是硬件自动压栈的,不需要软件或者编译器处理;
但是,如果不是CM3核的就不一定不需要压栈,如ARM9核的,参考《S3C2440全套中文手册》所述:
这里,R0~R7是共用的。中断发生后(切换到中断寄存器组),需先把需要用到的寄存器和LR(这里LR已经是中断寄存器组的LR,值为中断发生时,被中断处的PC值)压栈(这里的栈是指特定中断的栈,而不是被中断处的栈),如:
OS_CPU_ARM_ExceptIrqHndlr
SUB LR, LR, #4 ; LR offset to return from this exception: -4.
STMFD SP!, {R0-R12, LR} ; Push working registers.
上面的LR-=4,是因为3级流水线(取指令、翻译指令、执行指令)里,进入中断时,PC(取指令)是在正在执行的指令后面的第二条,而返回时应该执行后面的第一条。
然后退出中断时再出栈(同上,这里也是被中断处的栈)到寄存器和PC,并且,退出中断时的出栈记得要加^符号,以表示把SPSR里的值复制给CPSR,来恢复被中断处的程序状态/模式,如:
LDMFD SP!, {R0-R12, PC}^ ; Pull working registers and return from exception.
因为需要修改CPSR,所以对于ARM9而言,必须要用汇编来写中断例程。
另外,注意一点,C语言传参时,参数占用内存小于16时,用R0~R3保存,多余的保存到栈;但是C艹是全都保存到栈,然后把R0设置为栈地址,故参数小时,C语言调用函数开销会略小于C艹。