ARM汇编

来源:互联网 发布:起点写作软件 编辑:程序博客网 时间:2024/05/17 06:17
ADS1.2在汇编代码中调用C函数
来源:www.another-prj.com

对于ARM体系来说,不同语言撰写的函数之间相互调用(mix calls)遵循的是 ATPCS(ARM-Thumb ProcedureCallStandard),ATPCS主要是定义了函数呼叫时参数的传递规则以及如何从函数返回,关于ATPCS的详细内容可以查看ADS1.2Online Books ——Developer Guide的2.1节。这篇文档要讲的是汇编代码中对C函数调用时如何进行参数的传递以及如何从C函数正确返回

不同于x86的参数传递规则,ATPCS建议函数的形参不超过4个,如果形参个数少于或等于4,则形参由R0,R1,R2,R3四个寄存器进行传递;若形参个数大于4,大于4的部分必须通过堆栈进行传递。

我们先讨论一下形参个数为4的情况.
实例1:
test_asm_args.asm
//--------------------------------------------------------------------------------
       IMPORTtest_c_args ;声明test_c_args函数
       AREATEST_ASM, CODE, READONLY
       EXPORTtest_asm_args
test_asm_args
      STR lr, [sp,#-4]! 
;保存当前lr
      ldrr0,=0x10      
;参数 1
      ldrr1,=0x20       
;参数 2
      ldrr2,=0x30       
;参数 3
      ldrr3,=0x40      ;参数 4
       bltest_c_args     
;调用C函数
      LDR pc, [sp],#4  
;将lr装进pc(返回main函数) 
       END
test_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
//--------------------------------------------------------------------------------
       IMPORTtest_c_args ;声明test_c_args函数
       AREATEST_ASM, CODE, READONLY
       EXPORTtest_asm_args
test_asm_args
      STR lr, [sp,#-4]! 
;保存当前lr
      ldrr0,=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 入栈
      bltest_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函数) 
       END
test_c_args.c
//--------------------------------------------------------------------------------
void test_c_args(int a,int b,int c,int d,int e,int f,int g,inth)
{
      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://lionwq.spaces.eepw.com.cn/articles/trackback/item/17475





A.5.1  文件格式
       ARM 源程序文件(即源文件)为文件格式,可以使用任一文本编辑器编写程序代码。


       在一个项目中,至少要有一个汇编源文件或C 程序文件,可以有多个汇编源文件或多个C 程序文件,或者C程序文件和汇编文件两者的组合。

A.5.2  ARM 汇编的一些规范
       (1)汇编语句格式
       ARM 汇编中,所有标号必须在一行的顶格书写,其后面不要添加“:”,而所有指令均不能顶格书写。ARM汇编器对标识符大小写敏感,书写标号及指令时字母大小写要一致,在ARM 汇编程序中,一个ARM指令、伪指令、寄存器名可以全部为大写字母,也可以全部为小写字母,但不要大小写混合使用。注释使用“;”,注释内容由“;”开始到此行结束,注释可以在一行的顶格书写。
       格式:[标号] <指令|条件|S><操作数>[;注释]
       源程序中允许有空行,适当地插入空行可以提高源代码的可读性。如果单行太长,可以使用字符“”将其分行,“”后不能有任何字符,包括空格和制表符等。对于变量的设置,常量的定义,其标识符必须在一行的顶格书写。
       汇编指令正确的例子和错误的例子如下:
       正确的例子:
       
       Str1 SETS My string1.”   ;设置字符串变量Str1
       Count RN R0             ;定义寄存器名Count
       USR_STACK EQU 64          ;定义常量
       START LDR R0,=0x1123456   ;R0=0x123456H
       MOV R1,#0
       LOOP
       MOV R2,#3
       
       错误的例子:
       START MOV R0,#1          ;标号START没有顶格写
       ABC: MOV R1,#2         ;标号后不能带:
       MOV R2,#3                ;命令不允许顶格书写
       loop Mov R2,#3            ;指令中大小写混合
       B Loop                    ;无法跳转到Loop标号
       (2)标号
       在ARM 汇编中,标号代表一个地址,段内标号的地址在汇编时确定,而段外标号的地址值在连接时确定。根据标号的生成方式,可以有以下3钟:
        基于PC的标号
       基于PC 的标号时位于目标指令前的标号或程序中的数据定义伪指令前的标号,这种标号在汇编时将被处理成PC值加上或减去一个数字常量。它常用于表示跳转指令的目标地址,或者代码段中所嵌入的少量数据。
        基于寄存器的标号
       基于寄存器的标号通常用MAP 和FILED 伪指令定义,也可以用于EQU伪指令定义,这种标号在汇编时被处理成寄存器的值加上或减去一个数字常量。它常用于访问位于数据段中的数据。
        绝对地址
       绝对地址是一个32 为的数字量,它可以寻址的范围为0~232-1,可以直接寻址整个内存空间。
       (3)局部标号
       局部标号主要用于局部范围代码中,在宏定义也是很有用的。局部标号是一个0~99之间的十进制数字,可重复定义,局部标号后面可以紧接一个通常表示该局部变量作用范围的符号。局部变量的作用范围为当前段,也可以用伪指令ROUT来定义局部标号的作用范围。
       局部标号定义格式:N{routname}
       其中:N 局部标号,为0~99。
       routname 局部标号作用范围的名称,由ROUT 伪指令定义。
       局部标号引用格式:
       %{F|B}{A|T} N{routname}
       其中: % 表示局部标号引用操作。
       F 指示编译器只向前搜索
       B 指示编译器只向后搜索
       A 指示编译器搜索宏的所有嵌套层次
       T 指示编译器搜索宏的当前层
       如果F 和B 都没有指定,则编译器先向前搜索,再向后搜索。如果A 和T都没有指定,编译器搜索所有从宏的当前层次到宏的最高层次,比当前层次的层次不再搜索。
       如果指定了routname,编译器向前搜索最近的ROUT 伪指令,若routname与该ROUT伪指令定义的名称不匹配,编译器报告错误,汇编失败。
       示例如下:
       routintA ROUT
       
       3routineA
       BEQ %4routineA
       BGE %3
       4routineA
       
       otherstuff ROUT
       
       (4)符号
       在ARM汇编中,符号可以代表地址、变量、数字常量。当符号代表地址时又称为标号,符号就是变量的变量名、数字常量的名称、标号,符号的命名规则如下:
       a. 符号由大小写字母、数字以及下划线组成;
       b. 除局部标号以数字开头外,其它的符号不能以数字开头;
       c. 符号区分大小写,且所有字符都是有意义的;
       d. 符号在其作用域范围你必须是唯一的;
       e. 符号不能与系统内部或系统预定义的符号同名;
       f. 符号不要与指令助记符、伪指令同名。
       (5)常量
        数字常数
       数字常量有三种表示方式:
       十进制数,如:12,5,876,0。
       十六进制数,如0x4387,0xFF0, 0x1。
       n 进制数,用n-XXX 表示,其中n 为2~9,XXX 为具体的数。如2-010111,8-4363156等。
        字符常量
       字符常量由一对单引号及中间字符串表示,标准C语言中的转义符也可使用。如果需要包含双引号或“$”,必须使用“”和$$代替。如下示例:
       Hello SETS “Hello World!”
       Errorl SETS “The parameter ““VFH””error$$2”
        布尔常量
       布尔常量的逻辑真为{TRUE},逻辑假为{FALSE}。如下示例:
       testno SETS {FALSE}
       (6)段定义
       ARM 汇编程序设计采用分段式设计,一个ARM 源程序至少需要一个代码段,大的程序可以包含多个代码段及数据段。
       ARM 汇编程序经过汇编处理后生成一个可执行的映象文件,该文件通常包含下面3部分内容:
  • 一个或多代码段。代码段通常是只读的。
  • 零个或多个包含初始化值的数据段。这些数据段通常是可读写的。
  • 零个或多个不包含初始值的数据段。这些数据被初始化为0,通常中可读写的。

       连接器根据一定的规则将各个段安排到内存中的相应位置。源程序中段之间的相邻关系与执行的映象文件中段之间的相邻关系并不一定相同。
       代码段的例子如下:
       AREA Hello,CODE,READONLY ;声明代码段Hello
       ENTRY ;程序入口(调试用)
       START MOV R7,#10
       MOV R6,#5
       ADD R6,R6,R7 ;R6=R6+R7
       B ;死循环
       END
       每一个汇编文件都要以END 结束,包括*INC 文件,否则编译会有警告。
       数据段的例子如下:
       AREA DataArea,DATA,NOINIT,ALLGN=2
       DISPBUF SPACE 100
       RCVBUF SPACE 100
       
       (7)宏定义及其作用
       使用宏定义可以提高程序的可读性,简化程序代码和同步修改。ARM 宏定义与标准C的#define相似,只在源程序中进行字符代换。宏定义从MACRO 伪指令开始,到MEND 结束,并可以使用参数。
       宏要先定义,然后再使用。使用时直接书写宏名,并根据对应的宏定义格式设置输入参数或书写标号等。当源程序被汇编时,汇编编译器将展开每一个宏调用,用宏定义体代替程序中的宏调用,并使用实际的参数值代替宏定义时的形式参数。
       程序程序清单见后,程序中定义了一个宏CALL,用于调用子程序,调用时设置所要调用的子程序名$Function 及两个入口参数$dat1和$dat2。由于宏定义体中使用的是MOV 指令,所以$dat1 参数只能为8 位图的立即数或通用寄存器。
       宏应用的例子:
       
       MACRO ;宏定义
       CALL $Function,$dat1,$dat2 ;宏名称为CALL,带3 个参数
       IMPORT $Function ;声明外部子程序
       MOV R0,$dat1 ;设置子程序参数,R0=$dat1
       MOV R1,$dat2
       BL Function ;调用子程序
       MEND ;宏定义结束
       
       CALL FADD1,#3,#2 ;宏调用
       
       汇编预处理后,宏调用将被展开,程序清单如下:
       
       IMPORT FADD1
       MOV R0,#3
       MOV R1,#3
       BL FADD1
       

A.5.3  子程序的调用
       使用BL 指令进行调用,该指令会把返回的PC 值保存在LR,示例如下:
       
       BL DLEAY
       
       DELAY …
       MOV PC,LR
       当子程序执行完毕后,使用MOV、B/BX、STMFD 等指令返回,当然STMFD 要与LDMFD配套使用,子程序返回的方法:
       MOV PC,LR
       或 B LR
       或 BX LR
       或 STMFD SP!{R0-R7,PC }
       ARM7TDMI(-S)是没有BLX 指令的,但是可以通过几条程序实现其功能,模拟BLX 指令如下:
       ADR R1,DELAY+1
       MOV LR,PC ;保存返回地址到LR
       BX R1 ;跳转并切换指令集
       

A.5.4  数据比较跳转
       汇编程序可以使用CMP 指令进行两个数据比较,然后调高相应的ARM 条件码,实现跳转。代码例子如下:
       CMP R5,#10
       BEQ DOEQUAL ;若R5 为10,则跳转到DOEQUAL
       
       CMP R1,R2
       ADDHI R1,R1,#10 ;若R1>R2,则R1=R1+10
       ADDLS R1,R1,#5 ;若R1<=R2,则R1=R1+5
       
       ANDS R1,R1,#0x80 ;R1=R1&0x80,并设置相应标志位
       BNE WAIT ;若R1 的d7 位为则跳转到WAIT

A.5.5  循环
       下面的代码为循环程序的例子。例子指定循环次数,每循环一次进行减1 操作,并判断结果是否为0,若为0 则退出循环。
       MOV R1,#10
       LOOP … ;循环体
       SUBS R1,R1,#1
       BNE LOOP
       

A.5.6  数据块复制
       程序可以使用存储器访问指令LDM/STM 指令进行读取和存储,数据块复制示例如下:
       LDR R0,=DATA_DST ;指向数据目标地址
       LDR R1,=DATA_SRC ;指向数据源地址
       MOV R10,#10 ;复制数据个数为10*N 个字
       LOOP LDMIA R1!,{R2-R9} ;N 为LDM/STM 指令操作数据个数
       STMIA R0!,{R2-R9}
       SUBS R10,R10,#1
       BNE LOOP
       

A.5.7  栈操作
       ARM 使用存储器访问指令LDM/STM实现栈操作,用于子程序寄存器保存。注意,使用堆栈时,要先分配好堆栈空间,设置好寄存器R13(即堆栈指针SP),否则操作失败。
       OUTDAT
       STMFD SP!{R0-R7,LR}
       
       BL DELAY
       
       LDMFD SP!{R0-R7,PC}

A.5.8  特殊寄存器定义及应用
       基于ARM 核的芯片一般有片内外设,它们通过其特殊寄存器访问。片内外设的使用示例如下:
       WDTC EQU 0xE000000 ;寄存器定义
       
       LDR R0,=WDTC
       MOV R1,#0x12
       STR R1,[R0] ;WDTC=0x12
       散转功能
       散转是汇编程序常用的一种算法,其示例如下:
       CMP R0,#MAXINDEX ;判断索引号是否超出最大索引值
       ADDLO PC,PC,R0,LSL #2 ;若没有超出,则跳转到相应位置
       B ERROR ;若已经超出,则进行出错处理
                       ;散转表,对应索引号为0~N
       B FUN1
       B FUN2
       B FUN3
       

A.5.9  查表操作
       查表操作是汇编程序常用的一种操作,其示例如下:
       
       LDR R3,=DISP_TAB ;取得表头
       LDR R2,[R3,R5,LSL #2] ;根据R5 的值查表,取出相应的值
       
       ;下表为0--F 的字模
       DISR_TAB DCD 0xC0,0xF9,0xA4,0x99,0x92
       DCD 0x82,0xF8,0x80,0x90,0x88,0x83
       DCD 0xC6,0xa1,0x86,0x8E,0xFF

A.5.10  长跳转
       ARM 的B 和BL 指令不能全空间跳转,但通过对PC 进行赋值,实现32 位地址的跳转/调用,示例如下:
       ADD LR,PC,#4 ;保存返回地址,即RET_FUN
       LDR PC,[PC,#-4] ;跳转到LADR_FUN
       DCD LADR_FUN
       RET_FUN …
       也可使用伪指令LDR PC,=LADR_FUN 实现长跳转。

A.5.11  对信号量的支持
       ARM 提供一条内存与寄存器交换的指令SWP 用于支持信号量的操作,实现系统任务之间的同步或互斥,其使用的例子如下:
       DISP_SEM EQU 0x40002A00
       
       DISP_WAIT MOV R1,#0
       LDR R0,=DISP_SEM
       SWP R1,R1[R0] ;取出信号量,并设置其为0
       CMP R1,#0 ;判断是否有信号
       BEQ DISP_WAIT ;若没有信号,则等待
       

A.5.12  伪指令使用
       LDR 伪指令和NOP 伪指令应用例子代码如下:
       LDR R1,=0x12345678 ;加载32 位立即数
       LDR R0,=LDE_TAB ;加载标号地址
       NOP ;空指令
       B ;死循环

A.5.13  一个完整的例子
       下面是汇编程序完整的例子:
       ABC EQU 0x12
       ;声明一个代码段Example
       AREA Example,CODE,READONLY
       ENIRY
       CODE32
       ADR R0,Thumb_START+1 ;装载地址,并设置d0 位为1
       BX R0 ;切换到Thumb 状态
       CODE16 ;声明16 位代码(Thumb)
       Thumb_START
       MOV R1,#ABC
       ADD R1,R1,#0x10
       B Thumb_START
       END

A.5.14  外设控制
       在32 位的ARM核芯片中,其外设的控制寄存器中,一般会设置“置位/复位”寄存器,这样可以方便的实现位操作,而不会影响其它位,如       IOSET=0x01 只会将P0.1 的置位,而其它I/O 状态不变,另外,ARM存储/保存指令具有前偏移功能,所以对外设的控制寄存器进行操作时可使用此功能,避免了每次都加载寄存器地址的操作示例如下:
       LDR R0,=GPIO_BASE
       MOV R1,#0x00
       STR R1,[R0,#0x04] ;IOSET=0x00
       MOV R1,#0x10
       STR R1,[R0,#0x0C] ;IOCLR=0x101

A.5.15  三级流水线介绍
       ARM7TDM(-S)使用三级流水线执行指令,第一阶段从内存中取回的指令,第二阶段开始解码,而第三阶段实际执行指令。故此,程序计数器总是超出当前执行的指令两条指令。(在为跳转指令计算偏移量时必须计算在内)。因为有这个流水线,在跳转时丢失2个指令周期(因为要重新添满流水线)。所以最好利用条件执行指令来避免浪费周期。
       条件跳转示例:
       
       CMP R0,#0
       BEQ LOOP1
       MOV R1,#0x10
       MOV R2,#0x88
       LOOP1
       
       可以写为更有效的:
       
       CMP R0,#0
       MOVNE R1,#0x10
       MOVNE R2,#0x88
       

0 0
原创粉丝点击