ARMCC汇编方式

一、目的

本文分析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全套中文手册》所述:ARMCC汇编方式_第1张图片

ARMCC汇编方式_第2张图片

这里,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艹。

你可能感兴趣的:(编译原理)