C语⾔内联汇编
在阅读linux 源码的时候,我们会看到很多C语⾔内联汇编的代码。下⾯我们集中看看C语⾔内联汇编是怎么样的。
⾸先,我们得想想为什么会有在C语⾔⾥⾯内联汇编的需求。
主要有两个,⼀个是我们觉得在被频繁调⽤的函数,如果使⽤C写出来的代码,可能执⾏效率达不到我们的预期,于是我们就使⽤汇编语⾔来把这个函数的逻辑实现出来,例如qsort函数;
另⼀个是我们需要使⽤某些只能通过汇编指令才能实现的功能。可能有⼈会问,还有C语⾔⽆法实现的功能?这还真的有,例如开中断和关中断
#define sti() __asm__ ("sti"::)
#define cli() __asm__ ("cli"::)
显然就只能使⽤汇编指令来开中断和关中断了。
现在我们来看看内联汇编的⼀些规则。
内联汇编的⼀般格式如下:
asm [volatile] ( AssemblerTemplate
: [部分1OutputOperands]
[ : 部分2InputOperands
[ : 部分3Clobbers ] ])
;
⾸先! 内联汇编是⼀个statement ,也就是⼀条语句! 因此,⼀条内联汇编代码后边,需要跟着⼀个分号。
asm是关键字,告诉编译器之后紧挨着的第⼀个⼩括号内部的就是内联的汇编代码,⼀般也可以把asm写作__asm__。
volatile 也是关键字,如果写上volatile表⽰关闭编译器对这段汇编代码的优化。我们看看gcc官⽹对 volatile 的解释:
GCC’s optimizers sometimes discard asm statements if they determine there is no need for the output variables.
Also, the optimizers may move code out of loops if they believe that the code will always return the same result (i.e.
none of its input values change between calls). Using the volatile qualifier disables these optimizations. asm statements that have no output operands, including asm goto statements, are implicitly volatile.
也就是说,如何gcc编译器发现⼀段内联汇编代码的输出不被使⽤到,或者它发现在⼀个循环⾥⾯这段代码⼀直返回同⼀个值,那么它就会把这段内联汇编代码直接discard. 显然,对于⽤⼀个问题,有千千万万种写法,编译器只能做⼀些浅层的优化。当我们的代码写的⽐较复杂时,它将对我们的代码进⾏错误的优化,这是我们不想看到的,因此⼀般我们会加上这个volatile参数。
接下来是⼀个 AssemblerTemplate ,这个汇编模板要求是⼀个包含汇编指令的字符串,⾥⾯可以含有⼀些指向输⼊、输出操作数的占位符。gcc编译器通过⼀定的规则,将模板⾥⾯所有的占位符替换掉,并将替换后的结果输⼊到汇编器中。
因为汇编模板要求是⼀个字符串,那么如果我们有多条汇编指令,那怎么办呢?⼀个⽅法是将所有的汇编指令写在⼀⾏,这种⽅式当然可以,但是代码不美观。另外⼀种⽅法是利⽤C语⾔⾥⾯相邻字符串可以直接拼接成⼀个长的字符串这条规则,我们可以每⼀⾏写⼀条汇编指令,然后使⽤\ 将不同⾏的汇编指令字符串合并起来就可以了。例如⼀个合法的汇编模板可以是这样⼦的:
"mov %0,%1\n\t " \
"mov %1,%2\n\t"
它其实就是等价于:mov %0,%1\n\t mov %1 ,%2
其中以%开始的%1,%2···加做占位符,它由下⾯的输⼊输出操作数来决定。既然% 开始的都是占位符,那么我想输出%怎么办呢?⽐如我写了这么⼀条汇编模板:
"mov %eax,%ebx\n\t"
这是会报错的,因为gcc把%eax中的eax当做⼀个占位符了。此时,我需要使⽤%%来转义出%符号
ok,到⽬前为⽌,我们可能会对%0,%1,%2···产⽣疑问,这些占位符是如何与具体的某个数据产⽣关联的?
此时就需要介绍输出操作数和输⼊操作数了。
输出操作数紧跟着汇编模板,之间隔着⼀个:号。
输出操作数的格式为:
[asmSymbolicName1] constraint (cvariablename1),[asmSymbolicName1] constraint (cvariablename1)···
第⼀部分[asmSymbolicName1]叫做asm符号别名,就是相当于给后⾯的C语⾔变量cvariablename1设置⼀个汇编⾥⾯使⽤的别名。在汇编指令⾥⾯使⽤%[别名]来访问这个变量。
这部分可以省略。另外编译器默认为内联汇编的每个输出、输⼊操作数设置⼀个0,1,2,3,4···的数字别名。按各个操作数出现的次序,依次给这些操作数设置对应序号的数字别名。这些数字别名,在汇编模板⾥⾯使⽤%数字来访问。
举个例⼦:
int sum(int a,int b)
{
int rst = 0;
__asm__ volatile("addl %1,%2\n\t"\
"addl %3,%2\n\t"\
"mov %2,%[rst]\n\t"\
"mov %%eax,%2\n\t"
:[rst]"+r"(rst)
:"a" (a),
"b"(b),
"c"(123456)
:);
return rst;
}
输出操作数有:[rst]"+r"(rst),因此,在汇编模板中%[rst]和%0都是与输出变量rst绑定。
第⼆部分是⼀个约束字符串。约束字符串给出了程序员对编译器在转换汇编模板时候的⼀些建议。注意,只是建议。常见约束有
/onlinedocs/gcc/Simple-Constraints.html#Simple-Constraints
最后⼀个(cvariablename1) 通过⼀个括号将所指向的C语⾔变量指⽰出来。
整个输出操作数的描述可以没有。
下⼀部分的输⼊操作数的原理同输出部分。
最后⼀个所谓的破坏域声明,这个⼀般可不填,也只是给编译器提供的建议⽽已。
实例分析
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ (
"movw %%dx,%%ax\n\t" \
"movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" \
"movl %%edx,%2" \
: \
: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"o" (*((char *) (gate_addr))), \
"o" (*(4+(char *) (gate_addr))), \
"d" ((char *) (addr)),"a" (0x00080000))
输⼊操作数描述部分:
c语言中文网汇编语言:
"i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"o" (*((char *) (gate_addr))), \
"o" (*(4+(char *) (gate_addr))), \
"d" ((char *) (addr)),"a" (0x00080000))
说明:%0 指向表达式((short) (0x8000+(dpl<<13)+(type<<8))),其约束i表明它是⼀个⽴即数;
%1指向表达式*((char *) (gate_addr)),其约束o表⽰这个表达式的值是⼀个内存地址===>就是会翻译成⼀个地址⽽不是⽴即数$xxxxxx。%2指向表达式*(4+(char *) (gate_addr)),其约束o表⽰这个表达式的值也是⼀个内存地址。
%3指向表达式((char *) (addr)),其约束d表⽰汇编指令的在执⾏前先将(char*)(addr)的值给edx;
%4指向表达式(0x00080000)), 其约束a表⽰汇编指令在执⾏前会先将(4+(char*)(addr))的值给eax;
注意:在AT&T汇编中,⽴即数和直接内存访问时不同的。
例如:
head.S中
movl $0x10,%eax # reload all the segment registers
mov %ax,%ds # after changing gdt. CS was already
mov %ax,%es # reloaded in 'setup_gdt'
mov %ax,%fs
mov %ax,%gs
lss stack_start,%esp
xorl %eax,%eax
1: incl %eax # check that A20 really IS enabled
movl %eax,0x000000 # loop forever if it isn't
movl $0x10,%eax中的0x10是⽴即数,因为前⾯有个$
⽽movl %eax,0x000000中的0x0000000却是内存偏移量,表⽰ ds指⽰的段基址+0x0000000 所指向的内存区域。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论