C语言是一个系统级的语言,是一个可以直接深入到硬件最底层操作的语言。在前边的部分博客中,我们提到过,对硬件的操作实际上是对硬件控制器中的寄存器或存储单元进行操作,而在ARM架构中,这些寄存器或存储单元一般是以内存映射的方式进行访问的。在C语言中,指针刚好提供了访问任意的内存地址的方式,因此从语言上是可以表达的。但实际情况呢?
实际上,在操作底层硬件时,C语言还是有其局限性。有些硬件地址是没有地址一说的。比如处理器的寄存器,协处理器和协处理器的寄存器,系统控制器等。这些硬件资源是不可能使用C语言指针来访问的,这时就只好应用汇编指令了。
所以才有了今天的主题---C语言与汇编语言混合编程:
对于C语言与汇编语言混合编程使用的方式主要有如下几种情况:
1)汇编程序中调用C语言函数
2)汇编程序中使用C语言中定义的全局变量
3)C语言中调用汇编语言的函数
4)C语言中使用汇编程序中定义的全局变量
5)C语言中内嵌汇编指令(这种和编译器有关,不同编译器有不同的方式,这里讨论GUN GCC的内嵌方式,其余的参考相关)
一.C语言调用汇编函数
#文件名:a.s
.globl add
add:
popl �x;
popl �x;
movl (�x),�x;
addl $1,�x;
movl �x,(�x)
pushl �x;
pushl �x;
ret
编写a.c如下
#include
extern int add(int i);
int main()
{
int i=0;
add(&i); //调用汇编函数
printf("%d\n",i);
}
运用如下命令进行编译
arm-linux-gcc a.c a.S –O test
这时会生成一个test文件,将其拷贝到ARM目标机文件系统中,在shell下执行就可以了。
例子完了,总结一下C语言调用汇编函数有一下几个要点:
1.C语言中用extern
将用到的函数声明为外部函数
2.汇编中用.globl或.global将函数的标号声明为全局类型
3.汇编函数的写法遵循ATPCS约定
二.汇编语言中使用C全局变量
这个例程在汇编中实现对C语言的某个全局变量的求平方运算
//文件名:square.c
//说明:求平方,汇编使用C全局变量
#include
int var; //这个变量的值在汇编中修改
extern void asm_square();
int main()
{
var = 2;
asm_square();
printf("var = %d\n", var);
return 0;
}
汇编源码如下:
@文件名:asm_square.S
@说明:求平方,汇编使用C全局变量
.text
.globl asm_square
.extern var @ 可以不做这个申明
asm_square:
ldr r0, =var @得到var的地址
ldr r1, [r0] @得到var的置
mul r2, r1, r1 @乘法
str r2, [r0] @将结果保存回var
mov pc, lr @返回
.ltorg @声明文字池,可省略
.end
需要说明的是在汇编语言中甚至不加任何声明就使用C程序中的全局变量,一般建议加上一个.extern声明以增强可读性。
三.内嵌汇编
如果只是在C程序中有一小段需要用汇编语言实现的代码,那么单独写一个汇编源文件并进行相互调用就显得有点麻烦了,这时一个有用的方式就是内嵌汇编:
asm("指令序列":输出列表:输入列表:修改内容);
其中asm是关键字,除了指令序列外,其余的三部分都是可以省略的。但如果省略了中间的部分,相应的冒号不可以省略。指令序列不能省略,但可以为空字符串。
内嵌汇编使用举例如下:
asm(“mov %[result], %[value], ror
#1” :
[result]“=r” (y) : [value]“r” (x));
作用是将变量x循环右移1位并赋值给变量y。有关详细说明如下:
1)指令序列
在指令序列中可以使用输出列表和输入列表中定义的各种符号,如:%[value]和%[result]这些符号在生成指令时将替换成对应的内容。
2)输出列表和输入列表
这两个者都是由逗号隔开的多个部分组成,代表多个输出操作数。每个部分由一个方括号包围的符号名,一个对操作数使用方式进行限定的字符以及一个圆括号包围的C语言变量名组成。在早起的gcc版本中,输入输出列表不支持符号名。这是指令中必须以%0,%1的方式使用。不过现在gcc也紧跟升级却又不忘向前兼容。
输入输出列表中的限定符
限定符
含义
I
用立即数
J
范围在-4095~4095内的常数
K
按位取反的立即数
L
去相反数的立即数
l
寄存器r0-r7
M
0~32之间的常数或2的整数次幂
m
内存地址
r
寄存器r0-r15
X
任意用途
限定符前什么都不加的表示只读,加=表示只写,加+表示可读可写。其中只读的放在输入列表中,而只写和可读可写的操作数应放在输入列表中。
3)修改列表
修改列表是一系列逗号隔开的字符串,代表在嵌入式汇编中被修改过的内容。可以是一个寄存器名,表示这个寄存器的值被修改了,可以是CC,表示条件标志被修改了。编译器将根据这些信息在内嵌汇编的前后进行保护处理,以免破坏C程序本身对寄存器的使用。
特殊一点的当数memory,表示要修改内存中变量,这是编译器必须从内存加载变量的值而不能用缓存在寄存器的值,并且要在这条内嵌汇编语句结束后将变量的值写回内存。例如在Linux内核源码中经常用下面的内嵌汇编:
asm("":::"memory"); 这就是所谓的“内存屏障”编译器必须将它之前的变量写回内存,其后用到的变量必须从内存中加载。
另外需要注意的就是使用内嵌汇编,它仍然接受编译器的优化,甚至有可能被完全去掉。可以是用volatile关键字来防止这一点。如:
int temp;
asm volatile(
“mrs %0, cpsr\n\t”
“bic %0, %0, #0x80”\n\t
“msr CPSR_c, %0”
: “=r” (temp)
:
: “memorg”);
这段代码的作用是禁止中断,如果要在宏定义中使用内嵌汇编,则asm和volatile关键字可能引起编译器的警告,这时可用__asm__及_volatile__代替。
在不同的内嵌汇编代码中相互跳转是不允许的,但在同一个内嵌汇编中可以进行跳转。这时一般可以会使用局部标签。如下:
int main()
{
char *strsrc
= "Welcome to XU";
char
dst[64];
asm
volatile(
“loop:\n\t
“ldrb ch,[src],#1\n\t”
“strb ch,[dest],#1\n\t”
“cmp ch,#0\n\t”
“bne loop\n\t“
:
: [src]“r”
(src),[dst]“r” (dst)
:“r3”,“CC”,“memory”);
printf("dst:%s\n“, dst);
return
0;
}