前面的章节对汇编的各种指令和用法都做了介绍,从本章开始具体介绍下如何直接在C及C++的高级语言里使用汇编代码,也就是通常所说的inline assembly(内联汇编)的方式...

    本文由zengl.com站长对
    http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。

    另外再附加一个英特尔英文手册的共享链接地址:
    http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)

    本篇翻译对应汇编教程英文原著的第392页到第400页,对应原著第13章 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为400 / 577)。

    前面的章节对汇编的各种指令和用法都做了介绍,从本章开始具体介绍下如何直接在C及C++的高级语言里使用汇编代码,也就是通常所说的inline assembly(内联汇编)的方式。

What Is Inline Assembly? 什么是内联汇编

    在介绍内联汇编之前,有必要先了解下C语言与汇编之间的关系,在Linux系统里,C语言编写的代码可以在gcc工具的编译下生成对应的汇编指令,如果你想查看某个C代码具体会生成哪些汇编指令,可以在编译时,向gcc编译器传递-S参数,这样gcc就会生成C代码的汇编版本,为了演示这种做法,我们需要先写个简单的C程式,例如下面的cfunctest.c :

/* cfunctest.c – An example of functions in C */

#include <stdio.h>

float circumf(int a)
{
    return 2 * a * 3.14159;
}

float area(int a)
{
    return a * a * 3.14159;
}

int main()
{
    int x = 10;
    printf("Radius: %d\n", x);
    printf("Circumference: %f\n", circumf(x));
    printf("Area: %f\n",area(x));
    return 0;
}
 

    上面的C代码里一共有三个函数,circumf函数用于根据半径参数计算出对应的圆周长,area函数则用于根据半径参数来计算圆面积,main是主入口函数,相当于汇编文件里的_start,在main主函数里通过调用circumf和area函数来计算出相关的周长和面积,并将这些计算结果通过printf库函数显示到屏幕上。

    下面就用gcc加-S参数来查看上面的C代码对应的汇编指令:

$ gcc -S cfunctest.c

    该命令会创建一个cfunctest.s文件,该文件里的内容如下:

	.file	"cfunctest.c"
	.text
.globl circumf
	.type	circumf, @function
circumf:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$8, %esp
	movl	8(%ebp), %eax
	addl	%eax, %eax
	movl	%eax, -8(%ebp)
	fildl	-8(%ebp)
	fldl	.LC0
	fmulp	%st, %st(1)
	fstps	-4(%ebp)
	flds	-4(%ebp)
	leave
	ret
	.size	circumf, .-circumf
.globl area
	.type	area, @function
area:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$8, %esp
	movl	8(%ebp), %eax
	imull	8(%ebp), %eax
	movl	%eax, -8(%ebp)
	fildl	-8(%ebp)
	fldl	.LC0
	fmulp	%st, %st(1)
	fstps	-4(%ebp)
	flds	-4(%ebp)
	leave
	ret
	.size	area, .-area
	.section	.rodata
.LC2:
	.string	"Radius: %d\n"
.LC3:
	.string	"Circumference: %f\n"
.LC4:
	.string	"Area: %f\n"
	.text
.globl main
	.type	main, @function
main:
	pushl	%ebp
	movl	%esp, %ebp
	andl	$-16, %esp
	subl	$32, %esp
	movl	$10, 28(%esp)
	movl	$.LC2, %eax
	movl	28(%esp), %edx
	movl	%edx, 4(%esp)
	movl	%eax, (%esp)
	call	printf
	movl	28(%esp), %eax
	movl	%eax, (%esp)
	call	circumf
	movl	$.LC3, %eax
	fstpl	4(%esp)
	movl	%eax, (%esp)
	call	printf
	movl	28(%esp), %eax
	movl	%eax, (%esp)
	call	area
	movl	$.LC4, %eax
	fstpl	4(%esp)
	movl	%eax, (%esp)
	call	printf
	movl	$0, %eax
	leave
	ret
	.size	main, .-main
	.section	.rodata
	.align 8
.LC0:
	.long	-266631570
	.long	1074340345
	.ident	"GCC: (Ubuntu/Linaro 4.5.3-12ubuntu2) 4.5.3"
	.section	.note.GNU-stack,"",@progbits


    上面汇编函数的定义方式已经在之前的"汇编函数的定义和使用"相关章节里详细介绍过了,都是用的标准的.type .... , @function的方式来声明的,另外在main函数里调用circumf及area函数的方式也都是采用的C风格方式,即通过内存栈来传递参数,只不过gcc编译器生成的汇编代码没有使用push指令将参数压入栈,而是直接用movl    %eax, (%esp)的指令将参数传递到栈顶所在的内存里。

    从这个简单的例子可以看到,gcc编译器所生成的某些汇编指令还是比较繁琐的,有的时候你可能为了优化代码,希望自己写汇编指令,或者使用一些编译器无法生成的指令,例如CPUID指令等,那么你可以采用如下三种方式来直接控制所生成的汇编指令:
  • Implement the function from scratch in assembly language code and call it from the C program.
    在单独的汇编文件里从头写一个完整的汇编函数,然后在C程式里调用该函数
  • Create the assembly language version of the C code using the -S option, modify the assembly
    language code as necessary, and then link the assembly code to create the executable.
    使用gcc加-S参数创建出C代码的汇编版本,然后对生成的汇编版本的代码进行一些必要的修改,最后将修改后的汇编代码链接为可执行文件
  • Create the assembly language code for the functions within the original C code and compile it
    using the standard C compiler.
    直接在C代码里混合写入汇编指令,然后直接用标准的C编译器进行编译
    前两种方式将在以后的章节里进行介绍,本章讲解的是最后一种方式:直接在C代码里写入汇编指令即内联汇编的方式。这种内联汇编的方式,可以将高级语言与汇编语言各自的优势充分发挥出来,并且可以有效的控制所生成的汇编指令。

Basic Inline Assembly Code 基本的内联汇编方法:

    下面就介绍下在C语言里使用内联汇编的基本方法。

The asm format 使用asm关键字:

    GNU C编译器使用asm关键字来嵌入汇编代码,asm关键字的格式如下:

asm( "assembly code");

    上面括号里的汇编代码必须遵从以下两个格式:
  • The instructions must be enclosed in quotation marks.
    汇编指令必须用引号括起来
  • If more than one instruction is included, the newline character must be used to separate each
    line of assembly language code. Often, a tab character is also included to help indent the assembly
    language code to make lines more readable.
    汇编指令之间必须使用"\n"换行符分割开来,通常还可以在换行符后面再添加一个"\t"的tab字符,让生成的汇编指令具有更好的可读性
    第二条规则之所以要用换行符分割开来,是因为汇编器在对asm里的汇编代码进行汇编时,要求每条指令都位于单独的行里,这样才能解析成功。由于某些汇编器还要求汇编指令都以tab符开头,以方便和标签名区分开来,当然,GNU的汇编器并不要求这么做,不过为了保持汇编代码书写上的一致性,很多程序员都会在自己的汇编代码里加入tab符,所以asm关键字里的汇编代码最好都是用"\n\t"(即换行符加tab符)来分割开。

    一个简单的内联汇编的例子如下:

asm ("movl $1, %eax\n\tmovl $0, %ebx\n\tint $0x80");

    上面的汇编代码里有两条MOV指令,和一个INT指令,这些指令之间使用"\n\t"分割开,当然你也可以像下面那样以一种更方便阅读的方式进行书写:

asm ( "movl $1, %eax\n\t"
        "movl $0, %ebx\n\t"
        "int $0x80");

    上面是C语言里字符串常量相互连接的一种方法,每段字符串里包含一条汇编指令,每段字符串都必须用引号括起来。

    asm关键字内联的汇编代码可以放置在C或C++代码的任意位置,如下面的asmtest.c程式:

/* asmtest.c – An example of using an asm section in a program*/

#include <stdio.h>

int main()
{
    int a = 10;
    int b = 20;
    int result;
    result = a * b;
    asm ( "nop");
    printf("The result is %d\n", result);
    return 0;
}
 

    上面的C程式里使用asm在中间加入了一个简单的nop指令,该汇编指令什么也不做,不过该汇编指令会被编译进最终的可执行文件里,我们可以使用gcc加-S参数来查看最终会生成哪些汇编指令:

$ gcc -S asmtest.c
$ cat asmtest.s 
	.file	"asmtest.c"
	.section	.rodata
.LC0:
	.string	"The result is %d\n"
	.text
.globl main
	.type	main, @function
main:
	pushl	%ebp
	movl	%esp, %ebp
	andl	$-16, %esp
	subl	$32, %esp
	movl	$10, 28(%esp)
	movl	$20, 24(%esp)
	movl	28(%esp), %eax
	imull	24(%esp), %eax
	movl	%eax, 20(%esp)
#APP
# 11 "asmtest.c" 1
	nop
# 0 "" 2
#NO_APP
	movl	$.LC0, %eax
	movl	20(%esp), %edx
	movl	%edx, 4(%esp)
	movl	%eax, (%esp)
	call	printf
	movl	$0, %eax
	leave
	ret
	.size	main, .-main
	.ident	"GCC: (Ubuntu/Linaro 4.5.3-12ubuntu2) 4.5.3"
	.section	.note.GNU-stack,"",@progbits
$ 


    可以看到在#APP ........#NO_APP之间的nop指令就是之前的asm关键字嵌入的内联汇编指令。

Using global C variables 在内联汇编里使用C程式里的全局变量:

    在内联汇编里可以直接使用C代码里的全局变量,如果你想使用局部变量,就需要使用下一章要介绍的扩展asm格式,下面的globaltest.c程式就演示了如何在内联汇编里使用全局变量:

/* globaltest.c - An example of using C variables */

#include <stdio.h>

int a = 10;
int b = 20;
int result;

int main()
{
    asm ( "pusha\n\t"
        "movl a, %eax\n\t"
        "movl b, %ebx\n\t"

        "imull %ebx, %eax\n\t"
        "movl %eax, result\n\t"
        "popa");
    printf("the answer is %d\n", result);
    return 0;
}
 

    上面的C代码里定义了a,b,result三个全局变量,然后在asm定义的内联汇编代码里先将a,b两个全局变量的值通过mov指令依次设置到EAX和EBX,接着使用imull指令将EBX与EAX里的值相乘,结果存储在EAX里,最后通过movl %eax, result指令将计算结果存储到result全局变量里,在C代码里就可以使用printf函数将result结果显示出来。

    可以使用gcc -S来查看会生成的汇编指令:

$ gcc -S globaltest.c
$ cat globaltest.s
	.file	"globaltest.c"
.globl a
	.data
	.align 4
	.type	a, @object
	.size	a, 4
a:
	.long	10
.globl b
	.align 4
	.type	b, @object
	.size	b, 4
b:
	.long	20
	.comm	result,4,4
	.section	.rodata
.LC0:
	.string	"the answer is %d\n"
	.text
.globl main
	.type	main, @function
main:
	pushl	%ebp
	movl	%esp, %ebp
	andl	$-16, %esp
	subl	$16, %esp
#APP
# 11 "globaltest.c" 1
	pusha
	movl a, %eax
	movl b, %ebx
	imull %ebx, %eax
	movl %eax, result
	popa
# 0 "" 2
#NO_APP
	movl	result, %edx
	movl	$.LC0, %eax
	movl	%edx, 4(%esp)
	movl	%eax, (%esp)
	call	printf
	movl	$0, %eax
	leave
	ret
	.size	main, .-main
	.ident	"GCC: (Ubuntu/Linaro 4.5.3-12ubuntu2) 4.5.3"
	.section	.note.GNU-stack,"",@progbits
$


    从上面的输出可以看到,a和b在data数据段都使用.globl声明为全局变量,在内联汇编里可以直接使用这些全局变量的标签名来访问到它们的内存里的值,result全局变量因为没有被初始化,所以使用.comm来进行声明。

    在上面的内联汇编里,还有一点需要注意的是,在内联汇编代码的开头,使用了pusha指令将所有通用寄存器的值压入到内存栈保存起来,在内联汇编代码执行完毕时,再使用popa指令来恢复所有通用寄存器的值,这是因为在内联汇编里所使用的寄存器可能是C代码里正在用到的,如果修改了C代码所使用的寄存器的值,很可能会产生意想不到的结果,所以有必要在开头使用pusha指令来保存工作环境,并在结束时使用popa来恢复工作环境。

Using the volatile modifier 使用volatile修饰符:

    有时候GCC编译器会对内联汇编代码进行一些优化,以方便提升性能,从而会导致实际生成的汇编指令,可能会与你在asm里写入的汇编代码有所不同,有的时候你可能不希望编译器去修改优化你的汇编指令,那么就可以使用volatile修饰符:

asm volatile ("assembly code");

Using an alternate keyword 使用asm的备用关键字:

    在ANSI C标准里会将asm关键字用于别的用途,所以如果你的C代码是用ANSI C的标准来写的话,就可以使用__asm__这个备用的关键字来编写内联汇编(该关键字左右两侧各有两个下划线),如下所示:

__asm__ ( "pusha\n\t"
        "movl a, %eax\n\t"
        "movl b, %ebx\n\t"
        "imull %ebx, %eax\n\t"
        "movl %eax, result\n\t"
        "popa");

    同样volatile也有一个类似的备用关键字:__volatile__ ,如下所示:

__asm__ __volatile__ ( "pusha\n\t"
        "movl a, %eax\n\t"
        "movl b, %ebx\n\t"
        "imull %ebx, %eax\n\t"
        "movl %eax, result\n\t"
        "popa");

    以上就是内联汇编的基本用法,下一篇介绍内联汇编的扩展用法。

    OK,就到这里,休息,休息一下 o(∩_∩)o~~
上下篇

下一篇: 使用内联汇编 (二)

上一篇: 汇编里使用Linux系统调用 (三) 系统调用结束篇

相关文章

基本数学运算 (三) 除法和移位指令

汇编里使用Linux系统调用 (二)

什么是汇编语言(一) 汇编底层原理,指令字节码

高级数学运算 (二) 基础浮点运算

汇编流程控制 (三) 流程控制结束篇

汇编中使用文件 (一)