Linux 嵌入式汇编
在Linux内核编程中需要完成大量与具体硬件相关的操作,由于C语言无法直接对硬件操作。GCC提供了嵌入式汇编的功能,即可以在C语言代码中内嵌汇编语言完成对硬件的直接操作。
下面先给出一个例子:
内嵌汇编格式:
asm( 汇编语句模板
:输出部分
:输入部分
:破坏描述
)
内嵌汇编格式总共4个部分:汇编语句、输出部分、输入部分、破坏描述。各个部分之间用英文中的分号(colon)隔开。
汇编语句模板部分必不可少
,其它3个部分可选。关键字asm,是告诉GCC编译器后面括号内的代码是内嵌汇编。
汇编语句模板
:
内嵌汇编语句模板是由汇编指令序列组成,这些汇编指令之间需要使用分隔符(delimiter)如:“;”、“\n”或者“\n\t”分开。除
了常规汇编中的操作数(立即数、寄存器、变量)之外,内嵌汇编中的汇编指令的操作数还可以是占位符,操作数占位符最多有10个,依次用%0,%1,%2,%3,···%9来表示,他们与操作数(包括输出列表、输入列表)出现的次序依次对应,代表相应的操作数。比如说示例程序中的占位符%0代表操作数output,占位符%1代表操作数input。
GCC默认占位符指示的操作数是长整型的(4个字节),但可以通过操作数宽度后缀,可以让占位符指示的操作数字(2个字节)或1个字节。
在%和序号之间插入字符“w”、“h”、“b”分别表示访问操作数中的低字、低字中的高字节、低字中的低字节。比如在示例程序中%1代表操作数input,%1、%w1、%h1、%b1分别对应数值0x12345678、0x5678、0x56、0x78。对应IA32体系结构中通用寄存器的访问方式。以寄存器%eax为例,%ax代表寄存其%eax的低字部分,%ah代表%ax的高字节部分、%al代表%ax的低字节部分。
注意:汇编指令后缀通常应该和占位符中指示操作数的宽度一致,否则会导致不可预料的错误发生。
输出列表:
输出列表中指示和描述了该段内嵌汇编的输出结果保存到什么变量中,这些变量通过占位符作为汇编语句中的操作数。在输出列表中多于1个变量时,变量之间用逗号隔开。这些变量之前的修饰字符串中必须包含修饰字符“=”,表示它是一个输出操作数。
输入列表:
输入列表中指示和描述了该段内嵌汇编的输入变量。这些变量也通过占位符作为汇编语句中的操作数。在输出列表中多于1个变量的情况下,变量之间用逗号隔开。同样,输入变量前也可以使用修饰字符来影响、改变编译器的行为。
修饰字符
:
修饰字符的作用主要是指示编译器如何处理其所修饰的变量、如何将变量与汇编指令中的操作数进行关联以及将变量放在寄存器中还是放在内存中等。
1、 寄存器绑定
(1)、“a”:该修饰字符要求编译器将被修饰的变量和寄存器%eax进行绑定。
(2)、“b”、“c”、“d”、“S”、“D”:分别要求编译器将被修饰的变量和寄存其%ebx、%ecx、%edx、%esi、%edi进行绑定
(3)、“q”:该修饰字符要求编译器将被修饰的变量与寄存器eax、ebx、ecx、edx中的任何一个进行绑定。便于优化寄存器的使用
(4)、“r”:该修饰字符要求编译器将被修饰的变量与寄存器eax、ebx、ecx、edx、esi、edi中的任何一个进行绑定。便于优化寄存器的使用
(5)、“A”:该修饰字符说明被修饰的变量是一个64位的数据,要求编译器将寄存器eax和ebx联合起来与该变量进行绑定。
(6)、“m”:该修饰字符说明被修饰的变量是一个内存变量,要求编译器不要将该变量与寄存器进行绑定
2、 匹配限制符
在汇编指令中很多操作数是先读后写型。比如:add %1,%0 那么占位符%0所指示的操作数就是先读后写形操作数。在做这次加法前,先要将%1、%0占位符所指示的变量先读入寄存器,然后相加,将结果再写回到%0所使用的寄存器。由于GCC不能很好地自动处理这种先读后写类型的操作数,所以不能完成预期任务。所以GCC引进了匹配限制符,帮助GCC处理这种先读后写类型的操作数。
匹配限制符是一位十进制的数字,共有10个,依次是:0、1、2、3、4、5、6、7、8、9,分别表示它修饰的操作数与占位符%0、%1、···%9对应的操作数相匹配。例如:使用“0”修饰在输入列表中一个变量,这就指修饰的变量与%0所指示的操作数是同一个操作数,需要将这个操作数进行先读后写处理。例子如下:
3、 输出变量修饰符:
(1)“&”:该修饰符只用于输出变量,表示输出变量不能和输入变量共用同一个寄存器。这样就可以避免很多隐性错误。例子如下:
(2)“=”、“+”:这两个修饰符只用修饰输出变量。其中“=”表示输出变量只写,也就是说,不预先读入该输出变量到对应的寄存器中;而修饰符“+”表示必须先将输出变量预先读入到对应寄存器中,用于处理先读后写型变量。例子如下:
破坏描述:
1、寄存器破坏描述:
一种机制,用来通知编译器的内嵌汇编中修改了哪些寄存器。否则,对寄存器的再次使用就可能导致错误,破坏描述就是起到这种作用。换句话说,就是通知编译器,哪些寄存器不能被隐式使用了。破坏描述符有逗号隔开的字符串组成,每个字符串描述一种情况,一般是寄存器名,此外还有内存“memory”。我们称形如:“%eax”、“%ebx”、“%ecx”等为寄存器破坏描述符,称“memory”为内存描述符。
对于绑定了的寄存器,不需要使用破坏描述符进行描述了。
2、内存破坏描述符:
内存破化描述符也称内存屏障(barrier)。在Linux内核源代码中/include/linux/compiler-gcc.h文件中有这样的一个宏定义barrier:
/* Optimization barrier */
/* The "volatile" is due to gcc bugs */
#define barrier() __asm__ __volatile__("": : :"memory")
该宏定义依赖于内存破坏描述符完成屏障工作,保障在屏障之前缓存在寄存器中的变量写回到内存中。避免因缓存变量到寄存器而形成变量的多份拷贝,进而导致程序失效。
对于系统程序而言,代码中存在一些边界,要在前面的指令执行完毕后,边界后的指令才能执行。也就是需要阻止编译器跨边界进行优化、重排指令顺序,这样保证了将前面缓存到寄存器的变量写回内存中去。C语言中的关键字“volatile”具有与内存破坏描述类似的功能,用于保证其所修饰的变量不会被优化、缓存到寄存器中。这样每次访问时,必须到对应的地址中去,避免了因优化导致的程序出错。