ARM汇编初识

来源:互联网 发布:java ee的jdk 编辑:程序博客网 时间:2024/06/01 10:05
最近在做一个项目,采用的平台为STM32 + FreeRtos,因为带领一帮新人做,每个人的水平参差不齐,但是经过一段时间的锻炼,看的出来每个人都有一定的成长。因此,我觉得,对于一个人的成长而言,兄弟般的感觉做事,就是一个字:爽!反过来对自己而言,也是相当有益的。最近在关于系统启动方面的初始化,以及因内存资源有限需要实时关注内存占用问题,以及对函数的调用关系。得时刻盯着这些个小伙子写的代码质量。于是,逼迫自己多钻研一下汇编知识,之前一直做linux的驱动和应用开发。没有详细关注arm汇编启动代码部分,这次趁机学习一下。。。。下面,贴一些arm汇编的基础知识,系个人转载用————————————————————————————分割线————————————————————————————————————————————一、初识ARM汇编原文:http://blog.csdn.net/ruixj/article/details/4779607对于ARM体系来说,不同语言撰写的函数之间相互调用(mix calls)遵循的是 ATPCS(ARM-Thumb Procedure Call Standard),ATPCS主要是定义了函数呼叫时参数的传递规则以及如何从函数返回,关于ATPCS的详细内容可以查看ADS1.2 Online Books ——Developer Guide的2.1节。这篇文档要讲的是 汇编代码中对C函数调用时如何进行参数的传递以及如何从C函数正确返回。   不同于x86的参数传递规则,ATPCS建议函数的形参不超过4个,如果形参个数少于或等于4,则形参由R0,R1,R2,R3四个寄存器进行传递;若形参个数大于4,大于4的部分必须通过堆栈进行传递。   我们先讨论一下形参个数为4的情况.实例1:test_asm_args.asm//--------------------------------------------------------------------------------        IMPORT test_c_args ;声明test_c_args函数         AREA TEST_ASM, CODE, READONLY        EXPORT test_asm_argstest_asm_args        STR lr, [sp, #-4]! ;保存当前lr        ldr r0,=0x10               ldr r0,=0x10       ;参数 1         ldr r1,=0x20               ldr r1,=0x20       ;参数 2        ldr r2,=0x30               ldr r2,=0x30       ;参数 3        ldr r3,=0x40       ;参数 4         bl test_c_args             ldr r3,=0x40       ;参数 4         bl test_c_args     ;调用C函数        LDR pc, [sp], #4            LDR pc, [sp], #4    ;将lr装进pc(返回main函数)         ENDtest_c_args.c//--------------------------------------------------------------------------------void test_c_args(int a,int b,int c,int d){        printk("test_c_args:/n");        printk("%0x %0x %0x %0x/n",a,b,c,d);}main.c//--------------------------------------------------------------------------------int main(){     test_asm_args();     for(;;);}   程序从main函数开始执行,main调用了test_asm_args,test_asm_args调用了test_c_args,最后从test_asm_args返回main。代码分别使用了汇编和C定义了两个函数,test_asm_args 和 test_c_args,test_asm_args调用了test_c_args,其参数的传递方式就是向R0~R3分别写入参数值,之后使用bl语句 对test_c_args进行调用。其中值得注意的地方是用红色标记的语句,test_asm_args在调用test_c_args之前必须把当前的 lr入栈,调用完test_c_args之后再把刚才保存在栈中的lr写回pc,这样才能返回到main函数中。   如果test_c_args的参数是8个呢?这种情况test_asm_args应该怎样传递参数呢?实例2:test_asm_args.asm//--------------------------------------------------------------------------------        IMPORT test_c_args ;声明test_c_args函数         AREA TEST_ASM, CODE, READONLY        EXPORT test_asm_argstest_asm_args       STR lr, [sp, #-4]!         ENDtest_c_args.c//--------------------------------------------------------------------------------void test_c_args(int a,int b,int c,int d){        printk("test_c_args:/n");        printk("%0x %0x %0x %0x/n",a,b,c,d);}main.c//--------------------------------------------------------------------------------int main(){     test_asm_args();     for(;;);}   程序从main函数开始执行,main调用了test_asm_args,test_asm_args调用了test_c_args,最后从test_asm_args返回main。代码分别使用了汇编和C定义了两个函数,test_asm_args 和 test_c_args,test_asm_args调用了test_c_args,其参数的传递方式就是向R0~R3分别写入参数值,之后使用bl语句 对test_c_args进行调用。其中值得注意的地方是用红色标记的语句,test_asm_args在调用test_c_args之前必须把当前的 lr入栈,调用完test_c_args之后再把刚才保存在栈中的lr写回pc,这样才能返回到main函数中。   如果test_c_args的参数是8个呢?这种情况test_asm_args应该怎样传递参数呢?实例2:test_asm_args.asm//--------------------------------------------------------------------------------        IMPORT test_c_args ;声明test_c_args函数         AREA TEST_ASM, CODE, READONLY        EXPORT test_asm_argstest_asm_args       STR lr, [sp, #-4]! ;保存当前lr       ldr r0,=0x1 ;参数 1        ldr r1,=0x2 ;参数 2        ldr r2,=0x3 ;参数 3        ldr r3,=0x4 ;参数 4              ldr r0,=0x1 ;参数 1        ldr r1,=0x2 ;参数 2        ldr r2,=0x3 ;参数 3        ldr r3,=0x4 ;参数 4        ldr r4,=0x8       str r4,[sp,#-4]! ;参数 8 入栈       ldr r4,=0x7       str r4,[sp,#-4]! ;参数 7 入栈       ldr r4,=0x6       str r4,[sp,#-4]! ;参数 6 入栈       ldr r4,=0x5       str r4,[sp,#-4]! ;参数 5 入栈       bl test_c_args_lots       ADD sp, sp, #4     ; 清除栈中 参数 5,本语句执行完后sp指向 参数6        ADD sp, sp, #4     ; 清除栈中 参数 6,本语句执行完后sp指向 参数7        ADD sp, sp, #4     ; 清除栈中 参数 7,本语句执行完后sp指向 参数8        ADD sp, sp, #4     ; 清除栈中 参数 8,本语句执行完后sp指向 lr        LDR pc, [sp],#4            str r4,[sp,#-4]! ;参数 8 入栈       ldr r4,=0x7       str r4,[sp,#-4]! ;参数 7 入栈       ldr r4,=0x6       str r4,[sp,#-4]! ;参数 6 入栈       ldr r4,=0x5       str r4,[sp,#-4]! ;参数 5 入栈       bl test_c_args_lots       ADD sp, sp, #4     ; 清除栈中 参数 5,本语句执行完后sp指向 参数6        ADD sp, sp, #4     ; 清除栈中 参数 6,本语句执行完后sp指向 参数7        ADD sp, sp, #4     ; 清除栈中 参数 7,本语句执行完后sp指向 参数8        ADD sp, sp, #4     ; 清除栈中 参数 8,本语句执行完后sp指向 lr        LDR pc, [sp],#4     ;将lr装进pc(返回main函数)        ENDtest_c_args.c//--------------------------------------------------------------------------------void test_c_args(int a,int b,int c,int d,int e,int f,int g,int h){       printk("test_c_args_lots:/n");       printk("%0x %0x %0x %0x %0x %0x %0x %0x/n",              a,b,c,d,e,f,g,h);}main.c//--------------------------------------------------------------------------------int main(){     test_asm_args();     for(;;);}这部分的代码和实例1的代码大部分是相同的,不同的地方是test_c_args的参数个数和test_asm_args的参数传递方式。在test_asm_args中,参数1~参数4还是通过R0~R3进行传递,而参数5~参数8则是通过把其压入堆栈的方式进行传递,不过要注意这四个入栈参数的入栈顺序,是以参数8->参数7->参数6->参数5的顺序入栈的。直到调用test_c_args之前,堆栈内容如下:sp->+----------+        |  参数5  |       +----------+        |  参数6  |       +----------+        |  参数7  |       +----------+        |  参数8  |       +----------+        |   lr   |       +----------+test_c_args执行返回后,则设置sp,对之前入栈的参数进行清除,最后将lr装入pc返回main函数,在执行 LDR pc, [sp],#4 指令之前堆栈内容如下:        +----------+        |  参数5  |       +----------+        |  参数6  |       +----------+        |  参数7  |       +----------+        |  参数8  |sp->+----------+        |   lr   |       +----------+       ENDtest_c_args.c//--------------------------------------------------------------------------------void test_c_args(int a,int b,int c,int d,int e,int f,int g,int h){       printk("test_c_args_lots:/n");       printk("%0x %0x %0x %0x %0x %0x %0x %0x/n",              a,b,c,d,e,f,g,h);}main.c//--------------------------------------------------------------------------------int main(){     test_asm_args();     for(;;);}这部分的代码和实例1的代码大部分是相同的,不同的地方是test_c_args的参数个数和test_asm_args的参数传递方式。在test_asm_args中,参数1~参数4还是通过R0~R3进行传递,而参数5~参数8则是通过把其压入堆栈的方式进行传递,不过要注意这四个入栈参数的入栈顺序,是以参数8->参数7->参数6->参数5的顺序入栈的。直到调用test_c_args之前,堆栈内容如下:sp->+----------+        |  参数5  |       +----------+        |  参数6  |       +----------+        |  参数7  |       +----------+        |  参数8  |       +----------+        |   lr   |       +----------+test_c_args执行返回后,则设置sp,对之前入栈的参数进行清除,最后将lr装入pc返回main函数,在执行 LDR pc, [sp],#4 指令之前堆栈内容如下:        +----------+        |  参数5  |       +----------+        |  参数6  |       +----------+        |  参数7  |       +----------+        |  参数8  |sp->+----------+        |   lr   |       +----------+二、寄存器预览:原文:http://blog.csdn.net/bitowang/article/details/8488251BL NEXT ;跳转到子程序......... ;NEXT处执行NEXT..........MOV PC,LR ;从子程序返回这里的BL是跳转的意思,LR(R14)保存了返回地址PC(R15)是当前地址,把LR给PC就是从子程序返回这里有一下总结首先1.SP(R13) LR(R14)PC(R15)2.lr(r14)的作用问题,这个lr一般来说有两个作用:1》.当使用bl或者blx跳转到子过程的时候,r14保存了返回地址,可以在调用过程结尾恢复。2》.异常中断发生时,这个异常模式特定的物理R14被设置成该异常模式将要返回的地址。另外注意pc,在调试的时候显示的是当前指令地址,而用mov lr,pc的时候lr保存的是此指令向后数两条指令的地址,大家可以试一下用mov pc,pc,结果得到的是跳转两条指令,这个原因是由于arm的流水线造成的,预取两条指令的结果.3.》我以前看书不懂的地方子程序返回的三种方法现在总结如下1.MOV PC,LR2.BL LR3.在子程序入口处使用以下指令将R14存入堆栈STMFD SP!,{<Regs>,LR}对应的,使用以下指令可以完成子程序的返回LDMFD SP!, {<Regs>,LR}三、LDR伪指令原文:http://blog.csdn.net/axx1611/article/details/2335410/    由于我使用GNU工具链,所以以下的内容都以GNU AS的ARM语法为准。    LDR伪指令的语法形式如下:       LDR <reg>, = <constant-expression>    这个常量表达式<constant-expression>中可以包含Label(在ARM汇编中Label会在连接时解释为一个常数),且其中的常数前不加#符号。    范例demo.s:.equ    STACK_BASE0x0c002000.equ    STACK_SIZE0x00001000.text    ldr    sp, = STACK_BASE    ldr    sl, = STACK_BASE - STACK_SIZE    ldr    pc, =.equ    STACK_SIZE0x00001000.text    ldr    sp, = STACK_BASE    ldr    sl, = STACK_BASE - STACK_SIZE    ldr    pc, =.equ    STACK_SIZE0x00001000.text    ldr    sp, = STACK_BASE    ldr    sl, = STACK_BASE - STACK_SIZE    ldr    pc, = entry    这是一个合法的汇编文件,它把堆栈基址设为0x0c002000,栈限设为0x0c001000,然后跳到entry所标识的C程序中执行。    下面我们假设符号“entry”的地址为0x0c000000。    我们如果把上面代码写成:.text    mov    sp#0x0c002000    mov    sl#0x0c001000    mov    pc#0x0c000000    mov    sp#0x0c002000    mov    sl#0x0c001000    mov    pc#0x0c000000    汇编器会报错:        demo.s: Assembler messages:        demo.s:2: Error: invalid constant -- `mov sp,#0x0c002000'        demo.s:3: Error: invalid constant -- `mov sl,#0x0c001000'    说起这个错误的原因可就话长了,简而言之是因为RISC有一个重要的概念就是所有指令等长。在ARM指令集中,所有指令长度为4字节(Thumb指令是2字节)。那问题就来了,4字节是不可能同时存的下指令控制码和32位立即数的,那么我要把一个32位立即数(比如一个32位地址值)传送给寄存器该怎么办?    RISC CPU提供一个通用的方法就是把地址值作为数据而不是代码,从存储器中相应的位置读入到寄存器中,待会我们会看到这样的例子。    此外ARM还提供另一种方案。由于传送类指令的指令控制码部分(cond, opcode, S, Rd, Rn域)占去了20个字节,那能提供给立即数的就只剩12个位了。    ARM并未使用这12个位来直接存一个12位立即数,而是使用了类似有效数字一样的概念,只存8个位的有效位和一个4位的位偏移量(偏移单位为2)。这个东西在ARM被叫做术语immed_8,有兴趣的人可以找资料了解一下,到处都有介绍。    可以看出ARM的这个方法能直接使用的立即数是相当有限的,像0xfffffff0这样的数显然无法支持。别着急,ARM的传送类指令中还有一个MVN指令可以解决该问题。显然0x0000000f是一个有效立即数,MVN会先将其取反再传送,这样有效立即数的范围又扩充了一倍。    可就算如此仍有大量的32位立即数是无效的,比如上面那个例子中的0x0c002000和0x0c001000。面对这种问题一是使用RISC的通用方法,二是分次载入。    比如可以这样载入0x0c002000:.text    mov    sp#    mov    sp#0x0c000000    add    spsp#0x00002000    或者:.text    mov    sp#    mov    sp#0x0c000000    orr    spsp#0x00002000    感觉很狡猾是吧,呵呵。但是要注意它和方法一的一大区别:需要多条指令。那么在一些对指令数目有限制的场合就无法使用它,比如异常向量表处要做长跳转(超过±32MB)的话就只能用方法一;还有就是在同步事务中该操作不是原子的,因此可能需要加锁。    扯了这么多再回到LDR伪指令上来。显然上面的内容是复杂繁琐的,如果然程序员在写程序的时候还要考虑某个数是不是immed_8一定超级麻烦,因此为了减轻程序员的负担才引入了LDR伪指令。    你一定很好奇第一段代码demo.s被GNU AS变成了什么,好,让我们在Linux环境下执行下面的命令:        arm-elf-as -o demo.o demo.s        arm-elf-objdump -D demo.o   结果:demo.o:     file format elf32-littlearmDisassembly of section .text:Disassembly of section .text:00000000 <.text>:      0:   e59fd004        ldr     sp, [pc, #4]    ; c <.text+0xc>   4:   e59fa004        ldr     sl, [pc, #4]    ; 10 <.text+0x10>   8:   e59ff004        ldr     pc, [pc, #4]    ; 14 <.text+0x14>   c:   0c002000        stceq   0, cr2, [r0]    10:   0c001000        stceq   0, cr1, [r0]    14:   00000000        andeq   r0, r0, r0Disassembly of section .data:Disassembly of section .data:    由于entry还没连上目标地址,objdump反汇编会认为是0,我们先不管它。另外两条LDR伪指令变成了实际的LDR指令!但目标很奇怪,都是[pc, #4]。那好我们看看[pc, #4]是什么。    我们知道pc中存放的是当前指令的下下条指令的位置,也就是. + 8。那么上面的第一条指令ldrsp, [pc, #4]中的pc就是0x8,pc + 4就是0xc,而[0xc]的内容正是0x0c002000;同理,第二条ldr指令也是如此。显然这里LDR伪指令采用的是RISC通用的方法。    另外要说的是,如果LDR的是一个immed_8或者immed_8的反码数,则会直接被解释成mov或mvn指令。如ldr pc, = 0x0c000000会被解释成mov pc, 0x0c000000。    最后一点补充,我发现arm-elf-gcc通常都用累加法。如C语句中的i = 0x100ffc04;会变成类似于以下的语句:       mov   r0, #0x10000004       add   r0, r0, #0x000ff000       add   r0, r0, #0x00000c00       ...    原因不详。    另外要说的是,如果LDR的是一个immed_8或者immed_8的反码数,则会直接被解释成mov或mvn指令。如ldr pc, = 0x0c000000会被解释成mov pc, 0x0c000000。    最后一点补充,我发现arm-elf-gcc通常都用累加法。如C语句中的i = 0x100ffc04;会变成类似于以下的语句:       mov   r0, #0x10000004       add   r0, r0, #0x000ff000       add   r0, r0, #0x00000c00       ...    原因不详。    这是一个合法的汇编文件,它把堆栈基址设为0x0c002000,栈限设为0x0c001000,然后跳到entry所标识的C程序中执行。    下面我们假设符号“entry”的地址为0x0c000000。    我们如果把上面代码写成:.text    mov    sp#0x0c002000    mov    sl#0x0c001000    mov    pc#0x0c000000    mov    sp#0x0c002000    mov    sl#0x0c001000    mov    pc#0x0c000000    mov    sp#0x0c002000    mov    sl#0x0c001000    mov    pc#0x0c000000    汇编器会报错:        demo.s: Assembler messages:        demo.s:2: Error: invalid constant -- `mov sp,#0x0c002000'        demo.s:3: Error: invalid constant -- `mov sl,#0x0c001000'    说起这个错误的原因可就话长了,简而言之是因为RISC有一个重要的概念就是所有指令等长。在ARM指令集中,所有指令长度为4字节(Thumb指令是2字节)。那问题就来了,4字节是不可能同时存的下指令控制码和32位立即数的,那么我要把一个32位立即数(比如一个32位地址值)传送给寄存器该怎么办?    RISC CPU提供一个通用的方法就是把地址值作为数据而不是代码,从存储器中相应的位置读入到寄存器中,待会我们会看到这样的例子。    此外ARM还提供另一种方案。由于传送类指令的指令控制码部分(cond, opcode, S, Rd, Rn域)占去了20个字节,那能提供给立即数的就只剩12个位了。    ARM并未使用这12个位来直接存一个12位立即数,而是使用了类似有效数字一样的概念,只存8个位的有效位和一个4位的位偏移量(偏移单位为2)。这个东西在ARM被叫做术语immed_8,有兴趣的人可以找资料了解一下,到处都有介绍。    可以看出ARM的这个方法能直接使用的立即数是相当有限的,像0xfffffff0这样的数显然无法支持。别着急,ARM的传送类指令中还有一个MVN指令可以解决该问题。显然0x0000000f是一个有效立即数,MVN会先将其取反再传送,这样有效立即数的范围又扩充了一倍。    可就算如此仍有大量的32位立即数是无效的,比如上面那个例子中的0x0c002000和0x0c001000。面对这种问题一是使用RISC的通用方法,二是分次载入。    比如可以这样载入0x0c002000:.text    mov    sp#    mov    sp#    mov    sp#0x0c000000    add    spsp#0x00002000    或者:.text    mov    sp#    mov    sp#    mov    sp#0x0c000000    orr    spsp#0x00002000    感觉很狡猾是吧,呵呵。但是要注意它和方法一的一大区别:需要多条指令。那么在一些对指令数目有限制的场合就无法使用它,比如异常向量表处要做长跳转(超过±32MB)的话就只能用方法一;还有就是在同步事务中该操作不是原子的,因此可能需要加锁。    扯了这么多再回到LDR伪指令上来。显然上面的内容是复杂繁琐的,如果然程序员在写程序的时候还要考虑某个数是不是immed_8一定超级麻烦,因此为了减轻程序员的负担才引入了LDR伪指令。    你一定很好奇第一段代码demo.s被GNU AS变成了什么,好,让我们在Linux环境下执行下面的命令:        arm-elf-as -o demo.o demo.s        arm-elf-objdump -D demo.o   结果:demo.o:     file format elf32-littlearmDisassembly of section .text:Disassembly of section .text:Disassembly of section .text:00000000 <.text>:         0:   e59fd004        ldr     sp, [pc, #4]    ; c <.text+0xc>   4:   e59fa004        ldr     sl, [pc, #4]    ; 10 <.text+0x10>   8:   e59ff004        ldr     pc, [pc, #4]    ; 14 <.text+0x14>   c:   0c002000        stceq   0, cr2, [r0]      10:   0c001000        stceq   0, cr1, [r0]      14:   00000000        andeq   r0, r0, r0Disassembly of section .data:Disassembly of section .data:Disassembly of section .data:    由于entry还没连上目标地址,objdump反汇编会认为是0,我们先不管它。另外两条LDR伪指令变成了实际的LDR指令!但目标很奇怪,都是[pc, #4]。那好我们看看[pc, #4]是什么。    我们知道pc中存放的是当前指令的下下条指令的位置,也就是. + 8。那么上面的第一条指令ldrsp, [pc, #4]中的pc就是0x8,pc + 4就是0xc,而[0xc]的内容正是0x0c002000;同理,第二条ldr指令也是如此。显然这里LDR伪指令采用的是RISC通用的方法。    另外要说的是,如果LDR的是一个immed_8或者immed_8的反码数,则会直接被解释成mov或mvn指令。如ldr pc, = 0x0c000000会被解释成mov pc, 0x0c000000。    最后一点补充,我发现arm-elf-gcc通常都用累加法。如C语句中的i = 0x100ffc04;会变成类似于以下的语句:       mov   r0, #0x10000004       add   r0, r0, #0x000ff000       add   r0, r0, #0x00000c00       ...    原因不详。    另外要说的是,如果LDR的是一个immed_8或者immed_8的反码数,则会直接被解释成mov或mvn指令。如ldr pc, = 0x0c000000会被解释成mov pc, 0x0c000000。    最后一点补充,我发现arm-elf-gcc通常都用累加法。如C语句中的i = 0x100ffc04;会变成类似于以下的语句:       mov   r0, #0x10000004       add   r0, r0, #0x000ff000       add   r0, r0, #0x00000c00       ...    原因不详。    另外要说的是,如果LDR的是一个immed_8或者immed_8的反码数,则会直接被解释成mov或mvn指令。如ldr pc, = 0x0c000000会被解释成mov pc, 0x0c000000。    最后一点补充,我发现arm-elf-gcc通常都用累加法。如C语句中的i = 0x100ffc04;会变成类似于以下的语句:       mov   r0, #0x10000004       add   r0, r0, #0x000ff000       add   r0, r0, #0x00000c00       ...    原因不详。    另外要说的是,如果LDR的是一个immed_8或者immed_8的反码数,则会直接被解释成mov或mvn指令。如ldr pc, = 0x0c000000会被解释成mov pc, 0x0c000000。    最后一点补充,我发现arm-elf-gcc通常都用累加法。如C语句中的i = 0x100ffc04;会变成类似于以下的语句:       mov   r0, #0x10000004       add   r0, r0, #0x000ff000       add   r0, r0, #0x00000c00       ...    原因不详。    另外要说的是,如果LDR的是一个immed_8或者immed_8的反码数,则会直接被解释成mov或mvn指令。如ldr pc, = 0x0c000000会被解释成mov pc, 0x0c000000。    最后一点补充,我发现arm-elf-gcc通常都用累加法。如C语句中的i = 0x100ffc04;会变成类似于以下的语句:       mov   r0, #0x10000004       add   r0, r0, #0x000ff000       add   r0, r0, #0x00000c00       ...    原因不详。    最后一点补充,我发现arm-elf-gcc通常都用累加法。如C语句中的i = 0x100ffc04;会变成类似于以下的语句:       mov   r0, #0x10000004       add   r0, r0, #0x000ff000       add   r0, r0, #0x00000c00       ...    原因不详。    另外要说的是,如果LDR的是一个immed_8或者immed_8的反码数,则会直接被解释成mov或mvn指令。如ldr pc, = 0x0c000000会被解释成mov pc, 0x0c000000。    最后一点补充,我发现arm-elf-gcc通常都用累加法。如C语句中的i = 0x100ffc04;会变成类似于以下的语句:       mov   r0, #0x10000004       add   r0, r0, #0x000ff000       add   r0, r0, #0x00000c00       ...    原因不详。
看下面这个ARM汇编吧
 我们知道ARM CPU中有一条被广泛使用的指令LDR,它主要是用来从存储器(确切地说是地址空间)中装载数据到通用寄存器。但不论是ARMASM还是GNU ARM AS,都提供了一条与之同名的伪指令LDR,而在实际中使用该伪指令的情况也较多,那他们有什么不同呢?下面我谈谈我的理解。
0 0
原创粉丝点击