之前的内联汇编章节里,介绍了如何直接在C程式里嵌入汇编代码,但是这种方式不适合汇编代码比较多的情况,而且还受到约束符的限制,不同编译器对约束符的解释并不会完全相同...

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

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

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

    之前的内联汇编章节里,介绍了如何直接在C程式里嵌入汇编代码,但是这种方式不适合汇编代码比较多的情况,而且还受到约束符的限制,不同编译器对约束符的解释并不会完全相同,生成的最终指令也会多少有点出入。所以有时候,我们更希望是在单独的汇编文件里编写汇编函数,然后在C程式里调用这些函数,这种方式也便于大型项目的模块化开发。下面我们就对这种直接调用汇编模块里的函数的方式进行介绍。

Creating Assembly Functions 创建汇编函数:

    在之前的汇编函数的定义和使用相关章节里,介绍过如何创建汇编函数,以及如何创建C风格的汇编函数,这里先做个简单的回顾。

    为了让汇编函数能被C程式正常调用,在创建汇编函数时,最好遵循C风格的函数要求来做,即将所有的输入参数都放置在内存栈里,当函数执行完时,再将输出结果通过EAX寄存器返回给C调用程式。

    C风格的汇编函数的栈结构,如下图所示:


图1

    上图里,栈中的Function parameter(函数参数)与Return Address(返回地址)都是在进入函数之前就已经准备好了的,在汇编函数里只需将Return Address之后的部分压入栈即可,由于ESP的值在PUSH,POP之类的操作时会被修改,所以需要EBP寄存器来定位参数与局部变量,在设置EBP之前,还需要将Old EBP Value(原来的EBP寄存器值)压入栈保存起来,ESP在赋值给EBP后,需要向下移动,为Local Variable(局部变量)预留空间。

    另外,由于某些寄存器,如EBX之类的,既可以用于C调用程式,又可以用于汇编函数,所以在为局部变量预留空间后,还需要将汇编函数里可能会修改的寄存器保存到栈里,汇编函数执行完时,再弹出栈,恢复寄存器原来的值。有的寄存器,如MMX,SSE之类的寄存器可以安全的用于汇编函数里,对这些寄存器的修改不会影响到外部的C调用程式,只有在使用一些通用寄存器时,才需要进行保存和恢复操作,下表显示了需要在汇编函数里进行保存和恢复的寄存器:

Register 寄存器 Status 用途
EBX Used to point to the global offset table; 
must be preserved 
通常作为内存偏移指针使用,如果在汇编函数里修改了该寄存器,就必须进行保存和恢复操作
EBP Used as the base stack pointer by the C program; 
must be preserved 
通常作为指针来引用函数的参数与局部变量,必须进行保存和恢复
ESP Used to point to the new stack location within the function; 
must be preserved 
栈顶指针寄存器,汇编函数执行完时,ESP必须能恢复到进入函数之前的值
EDI Used as a local register by the C program; 
must be preserved 
通常作为指针来引用某个内存位置,如果汇编函数里修改了该寄存器,就必须进行保存和恢复操作
ESI Used as a local register by the C program; 
must be preserved 
和EDI类似,也是通常作为指针来引用某个内存位置,如果修改了,也必须进行保存和恢复操作

    根据以上内容,下面的代码可以作为能被C程式调用的汇编函数的基本模板:

.section .text
.type funcName, @function
funcName:
    pushl %ebp
    movl %esp, %ebp
    subl $12, %esp
    pushl %edi
    pushl %esi
    pushl %ebx

    <function code>

    popl %ebx
    popl %esi
    popl %edi
    movl %ebp, %esp
    popl %ebp
    ret

    上面的模板里,先将原来的EBP值压入栈,再将当前的ESP值,赋值给EBP,接着,通过SUB指令让ESP向低地址方向移动,从而为局部变量预留空间,最后将汇编函数里可能会用到的EDI,ESI,EBX寄存器的值压入栈,当函数执行完时,按照相反的顺序,先弹出寄存器的值,再恢复ESP与EBP的值,此时的ESP应该指向上面图1里的Return Address(返回地址),就可以用ret指令返回了。(如果在function code(汇编函数的主体代码里)并没有用到EDI,ESI或EBX的话,可以省略掉对应寄存器的保存和恢复操作)

    此外,如果在汇编函数里,声明了.data和.bss之类的数据段的话,这些段的内存区域会在编译时,与C调用程式的内存区域结合在一起,所以,可以将这些内存区域里的指针返回给C调用程式,从而在C程式里访问到汇编数据段里的数据。

Compiling the C and Assembly Programs 编译C和汇编程式:

    当C程式调用了汇编程式里的函数时,编译器需要对C程式与汇编程式都进行编译,才能生成最终的可执行文件,GNU C编译器提供了几种方式来生成这种可执行文件,下面就对这些方式进行介绍。

Compiling assembly source code files 直接编译C与汇编的源代码:

    假设C程式mainprog.c调用了asmfunc1.s里定义的asmfunc汇编函数,如果在编译过程中,没有指定asmfunc1.s的话,就会报类似如下的链接错误:

$ gcc -o mainprog mainprog.c
/tmp/cc9hGAnP.o: In function `main’:
/tmp/cc9hGAnP.o(.text+0xc): undefined reference to `asmfunc’
collect2: ld returned 1 exit status

$

    编译器会提示找不到asmfunc函数的相关定义,所以必须像下面那样将所需的汇编文件也提供给GCC:

$ gcc –o mainprog mainprog.c asmfunc1.s

    GCC编译器会对mainprog.c和asmfunc1.s都进行编译,并生成最终的mainprog可执行文件,如果mainprog.c依赖多个汇编文件里的函数,则需要将所有依赖的汇编文件名都提供给编译器,类似下面的命令:

$ gcc –o mainprog mainprog.c asmfunc1.s asmfunc2.s asmfunc3.s

    这种直接使用源代码文件名的方式,不会生成类似asmfunc1.o的中间目标文件,只会生成一个最终的可执行文件。

Using assembly object code files 使用目标文件进行编译:

    除了直接编译源代码的方式外,还可以先将汇编程式编译为中间的目标文件,再将目标文件与C程式进行编译:

$ as –o asmfunc.o asmfunc.s
$ gcc –o mainprog mainprog.c asmfunc.o

    上面的命令里,先将asmfunc.s通过as汇编器编译为asmfunc.o目标文件,再通过GCC将mainprog.c与asmfunc.o一起编译链接为mainprog的可执行文件(默认情况下,GCC在编译后,会自动调用ld链接器完成相关的链接工作)。

    同样的,如果mainprog.c依赖多个目标文件的话,也只需将所有依赖的目标文件都加入到GCC的命令行参数里即可:

$ gcc –o mainprog mainprog.c asmfunc1.o asmfunc2.o asmfunc3.o

    如果C程式所依赖的某个汇编程式被修改了,就必须按照上面介绍的两种方法,重新对C与汇编程式进行编译。

The executable file C调用汇编函数的完整例子:

    下面我们来看个简单的例子,在下面的asmfunc.s汇编文件里定义了一个asmfunc函数:

# asmfunc.s - An example of a simple assembly language function
.section .data
testdata:
    .ascii "This is a test message from the asm function\n"
datasize:
    .int 45
.section .text
.type asmfunc, @function
.globl asmfunc
asmfunc:
    pushl %ebp
    movl %esp, %ebp
    pushl %ebx
    movl $4, %eax
    movl $1, %ebx
    movl $testdata, %ecx
    movl datasize, %edx
    int $0x80
    popl %ebx
    movl %ebp, %esp
    popl %ebp
    ret
 

    上面asmfunc.s文件里定义的asmfunc函数遵循的是标准的C风格,由于汇编函数的主体代码要修改EBX寄存器,所以在开头,设置了EBP后,就通过pushl %ebx指令将EBX压入栈保存起来,在函数结束前,再通过popl %ebx指令来恢复原来的EBX的值。主体代码里通过Linux的write()系统调用(系统调用号为4,该值设置在EAX里),将testdata标签所引用的字符串显示到STDOUT(标准输出设备即屏幕,STDOUT的文件描述符为1,该值设置在EBX寄存器里)。有关系统调用的相关内容,请参考之前的汇编里使用Linux系统调用相关的文章。

   使用asmfunc函数的C代码定义在mainprog.c文件里:

/* mainprog.c - An example of calling an assembly function */
#include <stdio.h>

int main()
{
    printf("This is a test.\n");
    asmfunc();
    printf("Now for the second time.\n");
    asmfunc();
    printf("This completes the test.\n");
    return 0;
}

    汇编函数的调用方式和普通C函数的调用方式是一致的,只需指出函数名,同时将参数列表包含在括号里即可,下面是命令行中编译运行的情况:

$ gcc -o mainprog mainprog.c asmfunc.s
$ ./mainprog

This is a test.
This is a test message from the asm function
Now for the second time.
This is a test message from the asm function
This completes the test.

$

    程序的执行情况和预期的一致,asmfunc函数每次执行时,都会将汇编里的字符串信息通过write系统调用输出显示到屏幕上。

    另外,这里再介绍一个objdump工具,该工具可以对可执行文件进行反汇编,通过反汇编可以查看C程式与汇编程式实际生成的指令情况:

$ objdump -D mainprog > dump

    上面的命令会将objdump生成的反汇编信息输出到dump文件里,objdump命令的-D参数表示将所有段,包括数据段,都反汇编为指令代码格式,该反汇编工具在之前的汇编开发相关工具 (三) 工具介绍结束篇,kdbg,gprof,mepis的文章里详细的介绍过,如果想了解objdump更多的命令行参数的含义,可以参考这篇文章。

    我们可以使用文本编辑器打开生成的dump文件,从该文件里可以找到C程式的主入口函数的反汇编内容如下:

080483d4 <main>:
 80483d4:	55                   	push   %ebp
 80483d5:	89 e5                	mov    %esp,%ebp
 80483d7:	83 e4 f0             	and    $0xfffffff0,%esp
 80483da:	83 ec 10             	sub    $0x10,%esp
 80483dd:	c7 04 24 10 85 04 08 	movl   $0x8048510,(%esp)
 80483e4:	e8 07 ff ff ff       	call   80482f0 <puts@plt>
 80483e9:	e8 26 00 00 00       	call   8048414 <asmfunc>
 80483ee:	c7 04 24 20 85 04 08 	movl   $0x8048520,(%esp)
 80483f5:	e8 f6 fe ff ff       	call   80482f0 <puts@plt>
 80483fa:	e8 15 00 00 00       	call   8048414 <asmfunc>
 80483ff:	c7 04 24 39 85 04 08 	movl   $0x8048539,(%esp)
 8048406:	e8 e5 fe ff ff       	call   80482f0 <puts@plt>
 804840b:	b8 00 00 00 00       	mov    $0x0,%eax
 8048410:	c9                   	leave  
 8048411:	c3                   	ret    
 8048412:	90                   	nop
 8048413:	90                   	nop


    上面只是我的系统里编译器生成的指令情况,不同的编译器,或者相同编译器的不同版本,所生成的指令情况都会有所不同,可以看到,C程式里也是通过call指令来调用汇编程式里的asmfunc函数的。(上面的信息里,第一列是程序的线性地址,第二列是IA-32平台的指令字节码,第三列是反汇编后的汇编指令)

    我们还可以从dump文件里找到asmfunc函数的反汇编内容:

08048414 <asmfunc>:
 8048414:	55                   	push   %ebp
 8048415:	89 e5                	mov    %esp,%ebp
 8048417:	53                   	push   %ebx
 8048418:	b8 04 00 00 00       	mov    $0x4,%eax
 804841d:	bb 01 00 00 00       	mov    $0x1,%ebx
 8048422:	b9 14 a0 04 08       	mov    $0x804a014,%ecx
 8048427:	8b 15 41 a0 04 08    	mov    0x804a041,%edx
 804842d:	cd 80                	int    $0x80
 804842f:	5b                   	pop    %ebx
 8048430:	89 ec                	mov    %ebp,%esp
 8048432:	5d                   	pop    %ebp
 8048433:	c3                   	ret    
 8048434:	90                   	nop
 8048435:	90                   	nop
 8048436:	90                   	nop
 8048437:	90                   	nop


    上面输出的反汇编指令和asmfunc.s文件里的汇编代码基本上是一致的,只不过标签名被替换为了实际的内存地址。

Using Assembly Functions in C Programs C程式里使用汇编函数的返回值:

    在C程式里要正确调用某个汇编函数,就必须首先了解这些函数的输入参数与返回值的类型,下面就对汇编函数里常见的返回值类型进行介绍。

Using integer return values 使用整数类型的返回值:

    汇编函数最常见的就是返回整数类型的结果,返回的整数值会存储在EAX寄存器里,C程式里如果要处理这类结果的话,就必须定义一个整数类型的变量来存储结果,例如:

int result = function();

    上面例子里,function是一个汇编函数,该函数执行完后,会将结果存储在EAX里,接着C程式就会将EAX的结果存储到result所在的内存里。

    下面看个完整的例子,首先在square.s里定义一个汇编函数:

# square.s - An example of a function that returns an integer value
.type square, @function
.globl square
square:
	pushl %ebp
	movl %esp, %ebp
	movl 8(%ebp), %eax
	imull %eax, %eax
	movl %ebp, %esp
	popl %ebp
	ret


    上面的square函数会先将输入参数读取到EAX,然后通过imull %eax, %eax指令计算出平方值,并将结果也存储在EAX里进行返回,由于该函数并没用到EBX,EDI,ESI寄存器,所以在开头和结束代码里就没有这些寄存器的压栈和弹出栈的操作。

    下面的inttest.c程式就会调用上面的square函数,并将该函数返回的整数平方值给显示出来:

/* inttest.c - An example of returning an integer value */
#include <stdio.h>

int main()
{
	int i = 2;
	int j = square(i);
	printf("The square of %d is %d\n", i, j);

	j = square(10);
	printf("The square of 10 is %d\n", j);
	return 0;
}


    inttest.c程式里使用两个不同的输入参数调用了square函数两次,并通过printf函数将输入参数与返回的平方值打印显示出来。

    上面代码的编译和运行情况如下:

$ gcc -o inttest inttest.c square.s
$ ./inttest

The square of 2 is 4
The square of 10 is 100

$

    需要注意的是,如果使用的是64位的长整数,那么汇编函数的返回值应该放置在EDX : EAX的寄存器对里。

Using string return values 使用字符串类型的返回值:

    由于EAX寄存器里只能存储4个字节的数据(相当于4个字符),所以如果是字符串类型的返回值,则只能将字符串的32位的指针值(字符串的起始内存地址)存储到EAX里,进行返回,而C程式里则可以定义一个指针类型的变量来存储返回的字符串指针,并可以通过该指针值对字符串进行各种操作,字符串类型的返回值的大概原理图如下:


图2

    上图中,data string是定义在function汇编函数的内存空间里的一段字符串,C程式里在call function调用进入function函数后,该函数执行完时,会将data string的起始内存地址返回,C程式里会将该返回地址(即字符串的指针值)存储到stringvalue指针类型的变量里,接着就可以用printf函数将字符串信息显示出来,之所以C程式可以访问到汇编函数的内存数据,是因为在编译时,汇编模块的.data之类的内存数据段会合并到C主体程式的内存空间里。

    另外,需要注意的是,C程式使用的字符串都是以null字符(对应ASCII码为0)结尾的,所以汇编函数返回的字符串也必须始终以null字符结尾。

    要存储字符串类型的返回值,可以在C程式里定义char(字符)类型的指针变量:

char *result;

    上面的result是一个char类型的指针,可以指向单个字符,也可以指向以null结尾的字符串。

    默认情况下,C程式会假设函数的返回值是整数类型,如果某个汇编函数的返回值是字符串类型时,就需要通过prototype(函数原型)来声明该函数的返回值类型,当然prototype(原型)也可以同时声明函数的输入参数的类型,下面是一个函数原型的例子:

char *function1(int, int);

    上面的函数原型用于声明function1函数的两个输入参数都是整数类型,同时该函数的返回值是字符串类型的指针值,声明了函数原型后,在C程式里将该函数的返回值赋值给字符串指针类型的变量时,编译器就不会产生警告信息。

    函数原型必须在使用之前就进行声明,如果某个函数不需要输入参数的话,可以使用void关键字来指出:

char *function(void);

    在声明函数原型时,还必须注意结尾的分号,缺少分号会产生编译错误。

    下面就通过简单的例子来说明如何使用字符串类型的返回值,以及函数原型的用法。

    首先在cpuidfunc.s汇编模块里定义一个函数:

# cpuidfunc.s - An example of returning a string value
.section .bss
	.comm output, 13
.section .text
.type cpuidfunc, @function
.globl cpuidfunc
cpuidfunc:
	pushl %ebp
	movl %esp, %ebp
	pushl %ebx
	movl $0, %eax
	cpuid
	movl $output, %edi
	movl %ebx, (%edi)
	movl %edx, 4(%edi)
	movl %ecx, 8(%edi)
	movl $output, %eax
	popl %ebx
	movl %ebp, %esp
	popl %ebp
	ret


    上面的cpuidfunc函数里通过CPUID指令来获取处理器的供应商ID字符串信息,并将该字符串信息存储到.bss段定义的output标签处,最后将output标签所引用的内存地址存储到EAX寄存器进行返回。有关CPUID指令的用法可以参考之前的汇编开发示例 (一)的文章,这里只做一个简单的说明,在调用CPUID指令之前,可以将功能号存储到EAX寄存器,功能号为0,说明获取处理器供应商的ID字符串信息,在CPUID指令执行后,处理器会将供应商的ID字符串信息存放到EBX , EDX 及 ECX寄存器中,这三个寄存器里各自存放一部分字符串信息:
  • EBX里将包含字符串的前4字节
  • EDX里将包含字符串的中间4个字节
  • ECX里将包含字符串的最后4个字节
    所以在上面的代码里,就有movl %ebx, (%edi),movl %edx, 4(%edi)及movl %ecx, 8(%edi)这三条指令将字符串的三部分依次存储到output标签处。

    另外,.bss段里的内存数据默认是清零的,所以output标签所引用的字符串默认就是以null字符结尾的。

    下面的stringtest.c程式就声明了上面汇编函数的原型,同时在main主入口函数里调用了该函数:

/* stringtest.c - An example of returning a string value */
#include <stdio.h>

char *cpuidfunc(void);

int main()
{
	char *spValue;
	spValue = cpuidfunc();
	printf("The CPUID is: '%s'\n", spValue);
	return 0;
}


    上面代码先在开头声明了char *cpuidfunc(void);的函数原型,即该函数不需要输入参数,返回值为字符串指针类型,然后在main主入口函数里,调用cpuidfunc()函数,并将该函数的返回值赋值给spValue指针变量,最后通过printf函数将spValue指向的供应商ID字符串给显示出来。

    程序的编译和执行情况如下:

$ as -o cpuidfunc.o cpuidfunc.s
$ gcc -o stringtest stringtest.c cpuidfunc.o
$ ./stringtest

The CPUID is: 'AuthenticAMD'
$

    上面显示的是我的机子上的处理器供应商ID字符串信息,不同的处理器,显示的结果会有所不同。

    我们除了可以使用指针来返回字符串类型的数据外,还可以使用指针返回其他类型的数据(比如结构体等)。

    限于篇幅,本章就到这里,下一篇介绍汇编函数的其他类型的返回值(例如浮点数),以及汇编函数的输入参数相关的内容。

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

下一篇: 调用汇编模块里的函数 (二)

上一篇: 使用内联汇编 (三) 内联汇编结束篇

相关文章

汇编数据处理 (二)

汇编数据处理 (三) 浮点数

全书结束篇 使用IA-32平台提供的高级功能 (四) SSE2、SSE3相关指令

汇编开发相关工具 (二)

汇编数据处理 (四) 数据处理结束篇

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