查看文章 |
GCC中在C语言中嵌入汇编
2007-04-10 22:45
最开始,人们都是用汇编语言来写程序,甚至连操作系统也是用汇编语言写的。但是,当程序的复杂性非常高的时候用汇编就会很难控制了。因为汇编语言没有提供任何类型检查机制,因此,很容易会很基本的犯错误,比如,把指针作为整数来用而不是当作指针来引用内存。更糟糕的是,用汇编写的程序被限定在只能在特定的平台上运行,移植一个汇编程序到别的平台,甚至和重新写程序的难度差不多。但是,现在也有人热衷写汇编,比如:comp.lang.asm.x86,他们多数是写DOS下的游戏程序。
早期的高级语言编译器通常不能产生效率很高的代码也不能提供对底层硬件的访问, 因此不能满足编写系统程序的需要。那时候,人们为了写高效率的代码或者访问底层硬件需要写汇编代码,但是,现在,人们不是为了提高效率而需要汇编,因为现 代编译器产生的代码的效率和一个熟练的汇编程序员写的代码差不了多少。C语言提供了对各种硬件的操作,几乎所有的Linux代码都是C语言的。
但是,有的时候汇编是唯一的选择,nonehteless,there are times when writting assembly code is the only option.尤其是编写操作系统的时候。比如,有写CPU的寄存器保存着机器的状态信息,操作系统必须访问。还有一些特殊的指令需要汇编。甚至一些应用程序也需要汇编,比如访问状态字,C就不能直接做到。
面 临的挑战就是如何在大量的C代码中嵌入少量的汇编代码。一种方法是用汇编编写关键的功能,遵循C编译器的参数传递和寄存器使用规则。把汇编代码存入另外一 个文件,把C代码和汇编代码编译后利用链接器链接。例如,p1.c包含C语言代码,p2.s包含汇编语言代码,下面的编译命令:
gcc -o p p1.c p2.s
可以把p1.c编译,p2.s汇编,然后两个目标文件会被链接成一个可执行文件p.
在linux0.11内核中,setup.s就是利用这种类似的方法来调用main.c函数的。同时,中断处理函数也是用C写的而在汇编中进行调用。
另外一种方法就是把汇编和C代码混和来写。嵌入汇编允许用户直接向编译器编译产生的代码序列中插入汇编代码。With GCC,it is also possible to mix assembly with C code. Inline assembly allows the user insert assembly code directly into the code sequence generated by the complier. Features provided to specify instruction operands and to indicate to the compiler which are being overwritten by the assembly instructions. The resulting code is,of course,highly machine-dependent,since different types of machines do not have compatible machine insturctions. The asm directive is also specific to GCC, creating an incompatibility with many other compilers. Noneheless, this can be useful way to keep the amount of machine-dependent code to an absolute minimum.
GCC的帮助文档中有关于inline assembly的说明。可惜不是很完整和精确。
基本的inline assembly形式如下,有点类似函数调用:
asm(code-string)
code-string是要嵌入的汇编指令。编译器会把这段代码插入编译产生的汇编代码中。the compiler will insert this string verbatim into the assembly code being generated,and hence the compiler and the user-supplied assembly will be combined.因此,编译器产生的汇编代码和用户提供的汇编代码就结合到一起了。编译器不对嵌入的汇编代码进行语法检查,因此,第一个报错的通常是汇 编器。
下面通过一个例子来说明这种用法。
下面这段代码需要访问程序状态字,利用inline assembly来实现。
考虑下面两个函数原型:
int ok_smul( int x ,int y, int *dest)
int ok_umul(unsigned x, unsigned y, unsigned *dest)
每个函数的功能都是计算x和y的乘积然后把结果存入dest所指向的内存中。
返回值代表的是乘法结果是否溢出。返回0时代表溢出,返回1时代表没有溢出。我们用两个函数来实现乘法因为它们在不同的情况下溢出。
查一下IA32的指令mul和imul,可以发现当产生溢出时都会设置CF标志位。setae指令可以对寄存器的低字节清零当CF标志置位的时候,否则置1。我们期望利用如下代码实现上述功能;
/*frist attempt. doesnot work, why??*/
int ok_smull(int x, int y,int *dest)
{
int result=0;
*dest =x*y;
asm("setae %al");
return result;
}
这段代码不能达到我们的目的,因为我们是 期望%eax寄存器作为函数的返回值,期望编译器会把变量result的值放入%eax中作为返回值。不幸的是,GCC有它自己的规则。GCC产生的代码 会在最后初始化result的值,即GCC调换了result=0这指令的顺序,因为我们没有用到result。问题的关键是GCC不知道我们插入的汇编 代码和其余C代码想怎么样相互作用。
下面这种方法可以解决这个问题,但不是在任何情况下都工作。
/*second attempt, works with limited contexts*/
int dummy = 0;
int ok_smul2(int x,int y,int * dest)
{
int result;
*dest =x*y;
result=dummy;
asm("setae %al");
return result;
}
因为我们用全局变量来初始化result,编译器通常会比较“保守”当涉及到全局变量的问题,因为不会自做主张的改变代码顺序。
这程序只在编译的时候打开优化选项-O的时候才能正常工作。因为如果不优化,编译器会把result保存到堆栈上,并在返回之前读取它的值,这样会覆盖了setae指令设置的值。编译器不知道插入的汇编代码和剩余的代码的关系,因为我们没有提供这种信息。
为了解决这个问题,提供了扩展的嵌入式汇编。Extended form of asm
Gcc提供了一种扩展的asm,允许用户指定C语言中的变量值作为嵌入汇编语句的操作数和指定那些寄存器会被覆盖。有了这些信息,编译器就会正确地色互知要求的初始值,执行指令,并利用计算的结果。
一般形式的嵌入汇编语句如下:
asm(code-string [:output-list[:input-list [:overwrite-list]] ]);
register names such as "%eax" must be written with an extra ‘%’symbol,such as "%%eax".
下面的代码是采用这种方式的实现:
/*uses extended asm to get reliable code*/
int ok_suml3(int x,int y, int *dest)
{
int result;
*dest =x*y;
asm("setae %%bl; movzbl %%bl,%0"
: "=r" (result) /*output*/
: /*no input*/
: "%ebx" /*overwrite*/
);
return result;
}
|
最近读者: