欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C语言内联汇编

程序员文章站 2022-06-17 20:23:41
...

在阅读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绑定。
第二部分是一个约束字符串。约束字符串给出了程序员对编译器在转换汇编模板时候的一些建议。注意,只是建议。常见约束有https://gcc.gnu.org/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))

输入操作数描述部分:

:
	"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 所指向的内存区域。

相关标签: 内联汇编 kernal

上一篇: Java中的构造器

下一篇: 整数反转