历史上的今天

今天是:2024年10月21日(星期一)

正在发生

2018年10月21日 | ARM编程进阶之二 —— ATPCS与混合编程

发布者:美梦小狮子 来源: eefocus关键字:ARM编程  ATPCS  混合编程 手机看文章 扫描二维码
随时随地手机看文章

完全使用汇编语言来编写程序会非常的繁琐,因此通常情况下,只是使用汇编程序来完成少量必须由汇编程序才能完成的工作,而其它工作则由C语言程序来完成。这样一来,我们实际上就是在进行汇编和C的混合编程,甚至同一个程序的汇编源文件和C源文件是由不同的程序员编写的。在这种情况下,要想使不同程序员编写的汇编代码和C代码能耦合的很好,则必须有一个双方都必须遵守的规则,这就是ATPCS规则。

第一部分内容:ATPCS规则

ATPCS(ARM-Thumb Produce Call Standard)是ARM程序和Thumb程序中子程序调用的基本规则,目的是为了使单独编译的C语言程序和汇编程序之间能够相互调用。这些基本规则包括子程序调用过程中寄存器的使用规则、数据栈的使用规则和参数的传递规则。

1、寄存器的使用规则:

image

a)、寄存器R0 -- R11被分为2组:a1 -- a4,v1 – v8。所以对于兼容ATPCS的编译器而言,在编程的时候可以使用a1替换R0

b)、除了R13 -- R15有别名外,对于兼容ATPCS的编译器而言,也可以使用其它寄存器的别名:wr, sb, sl, fp, ip,它们都有自己的一些特殊用法

c)、寄存器R0 -- R3用于传递子程序的参数和返回结果(详见本文后部)

d)、寄存器a1 -- a4和ip是scratch寄存器(即:临时寄存器),其值在进行子程序调用时不需要保存和恢复。(详见本文后部)

2、数据栈的使用规则

在“其它寻址模式与其它指令”一文中,我讲到栈有4种类型:

FD (Full Descending) 满递减

ED (Empty Descending)空递减

FA (Full Ascending) 满递增

EA (Empty Ascending) 空递增

ATPCS规定数据栈为FD(满递减)类型,并且对数据栈的操作是8字节对齐的。这意味着我们在编写汇编子程序时,如果要进行出栈和入栈操作,则必须使用ldmfd和stmfd指令(或者ldmia和ldmdb);而兼容ATPCS的编译器在编译C代码时,也必须这样做。

3、参数的传递规则

参数个数固定的子程序参数传递规则:
    前4个整数参数,通过寄存器R0~R3来传递。其他参数通过数据栈传递。
子程序结果返回规则 
    结果为一个32位的整数时,必须通过寄存器R0返回;结果为一个64位整数时,通过寄存器R0和R1返回,依次类推。

下面看一下编译器对于这几个规则的遵循(实现)情况。

参数传递(4个参数)以及结果返回 :很显然,主调程序在调用子程序(即:bl func1)之前,将要传递给子程序的4个参数准备在了R0~R3中,从而使得子程序可以通过该4个寄存器获得转递给它的参数(即:4个参数是通过寄存器R0~R3来传递的);子程序在返回之前,将返回值放在了寄存器R0中,从而使得主调函数可以通过R0来获得子程序的返回值(即:结果为一个32位的整数时,通过寄存器R0返回)

image

多于4个参数,前4个参数通过寄存器R0~R3来传递,其他参数通过数据栈传递。

7个参数的情景。下面是程序

int func(int a, int b, int c, int d, int e, int f, int g)
{
    return(a+b+c+d+e+f+g);
}
int main()
{
    int a=1,b=2,c=3,d=4,e=5,f=6,g=7;
    return func(a,b,c,d,e,f,g);
}

的反汇编结果。我们通过它来分析一下当参数超过4个的时候,所谓“通过数据栈传递其它参数”是什么含义。

1 int func(int a, int b, int c, int d, int e, int f, int g)
2 {
3 func [0xe92d4010] stmfd r13!,{r4,r14}
4 000080ac [0xe59d4010] ldr r4,[r13,#0x10] //r4为第7个参数的值
5 000080b0 [0xe28de008] add r14,r13,#8 //r14指向了存放传入参数在栈中的位置
6 000080b4 [0xe89e5000] ldmia r14,{r12,r14} //r12为第5个参数的值,r14为第6个参数的值
7 return(a+b+c+d+e+f+g);
8 000080b8 [0xe0800001] add r0,r0,r1
9 000080bc [0xe0800002] add r0,r0,r2
10 000080c0 [0xe0800003] add r0,r0,r3
11 000080c4 [0xe080000c] add r0,r0,r12
12 000080c8 [0xe080000e] add r0,r0,r14
13 000080cc [0xe0800004] add r0,r0,r4
14 }
15 000080d0 [0xe8bd8010] ldmfd r13!,{r4,pc}
16 int main()
17 {
18 main [0xe92d401e] stmfd r13!,{r1-r4,r14}
19 int a=1,b=2,c=3,d=4,e=5,f=6,g=7;
20 000080d8 [0xe3a00001] mov r0,#1
21 000080dc [0xe3a0c002] mov r12,#2
22 000080e0 [0xe3a0e003] mov r14,#3
23 000080e4 [0xe3a04004] mov r4,#4
24 000080e8 [0xe3a01005] mov r1,#5
25 000080ec [0xe3a02006] mov r2,#6
26 000080f0 [0xe3a03007] mov r3,#7
27 return func(a,b,c,d,e,f,g);
28 000080f4 [0xe88d000e] stmia r13,{r1-r3} //main处的入栈操作,r1-r3实为占位符,是替第5、6、7个参数预先在栈内占位置的
29 000080f8 [0xe1a03004] mov r3,r4
30 000080fc [0xe1a0200e] mov r2,r14
31 00008100 [0xe1a0100c] mov r1,r12
32 00008104 [0xebffffe7] bl func
33 }
34 00008108 [0xe8bd801e] ldmfd r13!,{r1-r4,pc}

第3行采用的是stmfd指令实施入栈,这是因为要满足ATPCS中的“数据栈的使用规则”。而入栈的寄存器是r4和r14,r4入栈是因为r4在子程序中被破坏(使用)了,因此必须在子程序的入口入栈保存,在子程序的出口处出栈恢复(第28行);而r14要入栈则是因为r14存放的是子程序的返回地址,而r14又在子程序中被破坏(使用)了,如果不保存的话,在子程序返回(第34行)的时候,将不会正确地返回到主调程序。当然,你也许发现了r0,r1,r2,r3,r12同样在子程序中被破坏了,为什么它们不需要保存和恢复呢?这是因为“寄存器a1 -- a4和ip是scratch寄存器(即:临时寄存器),其值在进行子程序调用时不需要保存和恢复。”(见前文)。也就是说,对于主调程序的编写者而言,他应该很清楚他必须遵循ATPCS规则,所以他不会期望在子程序返回后,寄存器r0, r1, r2, r3, r12的值一定会维持原样。因此子程序的编写者也就不必保存和恢复这几个寄存器了,即使子程序破坏了它们的值。随便说一句,这条stmfd指令是由编译器自动加在子函数的第1条语句之前的,所以类推一下就应该明白,main函数运行时的第1条指令并不是程序员书写main函数的第1条语句,而是编译器添加的入栈指令。更进一步,为什么编译器要加这条入栈指令呢?因为main函数本质上也是个子函数而已,它也会被别人调用,也就是说,程序运行起来后,main函数并不是首先运行的。那么,是谁首先运行呢?当然是调用main函数的代码,这段代码被称之为:例行启动程序(boot routine),或称启动例程。它是由编译器在编译程序时自动加入的。

第20、21、22、23、29、30、31行显然是在准备(传递)前4个参数;第18、24、25、26、28行的执行,显然将后3个参数放到了栈中,而第4、5、6行完成后,子程序则将栈中的3个参数取出了。这样就完成了“多于4个的参数通过数据栈来传递”这个操作。

ATPCS

由此我们可以得到关于程序优化的一个结论:开始四个字大小的参数直接使用寄存器的R0-R3来传递(快速且高效的);如果需要更多的参数,将使用堆栈。(需要额外的指令和慢速的存储器操作) ;所以通常限制参数的个数,使它为4或更少,如果不可避免,把常用的参数前4个放在R0-R3中。

第二部分内容:C和ARM汇编程序间相互调用(点击下载示例代码)

在C和ARM汇编程序之间相互调用必须遵守ATPCS规则。C和汇编之间的相互调用可以从以下这四方面来说明:

在C语言程序中调用汇编程序
在汇编程序中调用C语言程序
汇编程序对C全局变量的访问
C程序对汇编全局变量的访问

C程序中内嵌汇编

1、在C语言程序中调用汇编子程序

为了保证程序调用时参数的正确传递,汇编程序的设计要遵守ATPCS。在汇编程序中需要使用EXPORT伪操作来声明,使得本程序可以被其它程序调用。同时,在C程序调用该汇编程序之前需要在C语言程序中使用extern关键词来声明该汇编程序。

参阅示例代码中xmain函数(在ledtest.c中)对delay函数(在delay.s中)的调用

extern int delay(int time);

EXPORT delay

2、在汇编程序中调用C语言子程序

为了保证程序调用时参数的正确传递,汇编程序的设计要遵守ATPCS。在C程序中不需要使用任何关键字来声明将被汇编语言调用的C程序(只要该程序的声明前不要加static关键字),但是在汇编程序调用该C程序之前需要在汇编语言程序中使用IMPORT伪操作来声明该C程序。在汇编程序中通过BL指令来调用子程序。

参阅示例代码中init.s文件中的代码对xmain函数的调用

IMPORT xmain
bl xmain

int xmain(int val)

3、汇编程序访问全局C变量

汇编程序可以通过C全局变量的地址间接访问在C语言程序中声明的全局变量。在汇编程序中,通过使用IMPORT关键词引人C全局变量,该C全局变量的名称在汇编程序中被认为是一个标号,从而汇编程序可以利用LDR和STR指令访问该标号所代表的地址处存放的内容(即:C全局变量的值)。

参阅示例代码中init.s文件中的如下几行:

IMPORT i
ldr r0, i
sub r0, r0, #1
str r0, i

对于不同类型的变量,需要采用不同选项的LDR和STR指令,如下所示:

unsigned char LDRB/STRB
unsigned short LDRH/STRH
unsigned int LDR/STR
char LDRSB/STRB
short LDRSH/STRH

4、C程序对汇编全局变量的访问

汇编程序中用DCD为全局变量分配空间并赋初值,并定义一个标号代表该存储位置,用EXPORT导出该标号。C程序将会将该标号视为全局变量的名称,在C程序中用extern声明该全局变量,之后就可以按正常的方式访问该全局变量了。

参阅示例代码中delay.s文件中的代码和xmain函数的代码:

    EXPORT DELAYVAL
DELAYVAL
    DCD 0xffff

extern int DELAYVAL;

5、C程序中内嵌汇编

有些操作C语言程序是做不了的,例如:改变cpsr寄存器的值、初始化堆栈指针寄存器sp,等等,它们只能由汇编程序完成。但出于编程简洁以及其它一些因素的考虑,有时我们需要在C源代码中实现上述的操作,此时我们就必须采用在C源代码中嵌入少量汇编代码的方法来实现,这就是C程序中的内嵌汇编。

内嵌的汇编指令包括大部分的ARM指令和Thumb指令,但是不能直接引用C的变量定义,数据交换必须通过ATPCS进行,不支持诸如直接修改PC实现跳转等底层功能。嵌入式汇编语句在形式上是独立定义的函数体,其语法格式为:
__asm
{
指令[;指令]
……
[指令]
}

其中“__asm”为内嵌汇编语句的关键字,需要特别注意的是前面有两个下划线。同一行如有多条指令,则指令之间用分号分隔,如果一条指令占据多行,除最后一行外都要使用连字符“\”

例如,如果我们需要在C程序中禁用中断,那么内嵌的汇编代码如下:

__asm
{
    MRS    R0 CPSR
    ORR    R0, R0,#0x80
    MSR    CPSR_c,R0
}

出于完整性的考虑,最后将内嵌汇编相对于一般汇编的一些不同的特点罗列如下:

操作数可以是寄存器、常量或C表达式。它们可以是char、short或者int类型,而且是作为无符号数进行操作 。
内嵌的汇编指令中使用物理寄存器有一些限制。
常量前的符号“#”可以省略
只有指令B可以使用C程序中的标号,指令BL不能使用C程序中的标号。 
不支持汇编语言中用于内存分配的伪操作。
指令中如果包含常量操作数,该指令可能会被汇编器展开成几条指令。 
内嵌汇编器不支持通过“·”指示符或PC获取当前指令地址;
不支持LDR Rn,= expression伪指令,而使用MOV Rn, expression指令向寄存器赋值;
不支持标号表达式;
不支持ADR和ADRL伪指令;
不支持BX和BLX指令;
不可以向PC赋值;
使用0x前缀替代“&”表示十六进制数。
必须小心使用物理寄存器,如R0~R3,LR和PC。
不要使用寄存器寻址变量。
使用内嵌汇编时,编译器自己会保存和恢复它可能用到的寄存器,用户无须保存和恢复寄存器。
LDM和STM指令的寄存器列表只允许物理寄存器。


关键字:ARM编程  ATPCS  混合编程 引用地址:ARM编程进阶之二 —— ATPCS与混合编程

上一篇:ARM编程进阶之三 —— 裸机硬件的控制方法与例程
下一篇:ARM指令状态切换到Thumb指令状态

推荐阅读

近日,国家知识产权局发布了《2017年我国人工智能领域主要统计数据报告》(下称《报告》)。根据统计,2017年中国人工智能发明专利申请公开量和发明专利授权量分别达到4.6284万件和1.7477万件。统计分析结果显示,2017年我国人工智能领域发明专利稳步增长,基础算法增速突出,基础算法和基础硬件领域高校、科研单位优势明显,企业在垂直应用领域占绝对优势...
Alphabet旗下的无人机交付创业公司Wing现已开始在美国弗吉尼亚州提供服务。该公司目前正在向弗吉尼亚州克里斯琴斯堡(Christiansburg)的居民提供小吃和保健品。Wing表示,这是美国第一个商业无人机交付服务。今年4月,Wing获得了美国联邦航空局(FAA)的批准,以在美国境内运营商业货物运输业务。在此之前,Wing在澳大利亚首都堪培拉郊外的三个郊区使用无...
据THE ELEC报道,美国针对华为的禁令给韩国Amotech的MLCC业务带来了负面影响。Amotech最初计划在2020年初向包括华为在内的中国公司提供用于电信组件的MLCC。但疫情大流行导致中国5G部署计划被推迟,该公司出货受到影响。再者,疫情也推迟了该公司MLCC的生产计划。不过在5月份,Amotech重新开始向中国的潜在客户供应MLCC样品。目前产品处于客户测试阶段。...
ATmega48/88/168具有片内能隙基准源,用于掉电检测,或者是作为模拟比较器或ADC的输入。电压基准的启动时间可能影响其工作方式。启动时间列于Table 23。为了降低功耗,可以控制基准源仅在如下情况打开:1. BOD 使能 ( 熔丝位BODLEVEL [2..0]被编程)2. 能隙基准源连接到模拟比较器(ACSR 寄存器的ACBG 置位)3. ADC 使能因此,当 BOD 被禁止时,...

史海拾趣

问答坊 | AI 解惑

44B0X中文资料.大家支持下

44B0X中文资料.大家支持下…

查看全部问答∨

钱学森最后一次系统谈话:大学要有创新精神

整理者注:钱老去世以后,许多人问我们:钱老有什么遗言?并希望我们这些身边工作人员写一篇“钱学森在最后的日子”的文稿。我们已告诉大家,钱老去世时很平静安详,他没有什么最后的遗言。因为在钱老去世前的一段日子,他说话已经很困难了。我们可 ...…

查看全部问答∨

PCB上的模拟地和数字地有什么区别

各位兄台,咨询个问题,PCB上的模拟地和数字地有什么区别?模拟地和数字地接到哪里去?信号地是不是也有讲究…

查看全部问答∨

硬件笔试问题,帮帮忙

画出Y="A"*B+C的cmos电路图。 这个"A"和A是什么关系…

查看全部问答∨

arm fpga 扩展 uart

wince 操作系统, arm9 + FPGA 来扩展多串口系统,请问wince 驱动如何写?…

查看全部问答∨

51单片机串口发送问题

为什么我这样不能发?那个TI不处理我至少应该收到一个啊?                              for(m=0;m<20;m=m+1)             ...…

查看全部问答∨

想学习arm,各位能否介绍一块板子

想学习arm,各位能否介绍一块板子…

查看全部问答∨

STM32由入门到精通2012年3月版

STM32由入门到精通2012年3月版,很值得看看!!!! …

查看全部问答∨
小广播
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件
随便看看

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

电子工程世界版权所有 京ICP证060456号 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved