一、创建汇编函数
下面显示把输入值存放到堆栈中的方式,以及汇编语言函数如何访问它们。
EBP寄存器用作访问堆栈中的值的基址指针。调用汇编语言的函数必须知道输入值按照什么
顺序存放在堆栈中,以及每个输入值的长度(和数据类型)。
在汇编函数代码中,C样式函数对于可以修改哪些寄存器和函数必须保留哪些寄存器有着特
定的规则。如果必须保留的寄存器在函数中被修改了,那么必须恢复寄存器的原始值,否则在执
行返回发出调用的C程序时也许会出现不可预料的结果。在函数中可以安全地使用MMX和SSE寄
存器,但使用通用寄存器和FPU寄存器时必须谨慎。
如下表所示,被调用的函数必须保留EBX、EDI、ESI、EBP和ESP寄存器。这就要求在执
行函数代码之前把寄存器的值压入堆栈,并且在函数准备返回调用程序时把它们弹出堆栈。
C函数调用的汇编语言函数的基本模板如下:
.section .text
.type func, @function
func:
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
二、编译C和汇编程序
1、编译汇编源代码文件
编译包含汇编语言函数的C程序时,编译器必须直到如何访问函数。如果编译器
不能解析程序中使用的函数,就会产生错误。
为了解决这个问题,在编译时,编译器必须可以利用汇编语言函数和C程序代
码。完成这一工作的一种方式是把汇编语言函数的源代码文件包含在汇编器命令行
中。
2、使用汇编目标代码文件
使用as命令进行汇编。对于大型项目可以使用make实用程序。
3、可执行文件
下面是例子:
.section .data
testdata:
.ascii "This is a test message from the asm fucntion\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
#include <stdio.h>
extern void asmfunc( void );
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;
}
最后用objdump -d选项查看生成的汇编语言代码:
三、在C程序中使汇编函数
1、使用整数返回值
最基本的汇编语言函数调用把32位整数值返回到EAX寄存器中。调用函数获得这
个值,它必须把返回值作为整数赋值给C变量:
int result = function();
C程序生成的汇编语言代码提取存放在EAX寄存器中的值,并且把它传送到分配
给C变量名称的内存位置(通常是堆栈中的局部变量)。这个C变量包含来自汇编语言函
数的返回值,可以在整个C程序中像以往那样使用它。
.type square, @function
.globl square
square:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
imull %eax, %eax
movl %ebp, %esp
popl %ebp
ret
#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;
}
对于64位长整数值,返回值存放在EDX:EAX寄存器对中。
2、使用字符串返回值
返回字符串函数返回指向字符串存储位置的指针。调用这个函数的C或者C++程序必须使用
指针变量保存返回值。然后可以通过指针值访问字符串。
字符串值被包含子啊函数的内存空间之内,但是主程序可以访问它,因为函数内存空间包含
在主程序的内存空间之内。函数返回的32位指针值是字符串开始位置所在的内存位置(在C和
C++程序中处理字符串值时,记住字符串必须使用空字符结尾)。
C和C++语言在变量名称前面使用星号表明这个变量包含一个指针。可以创建指向任何数据
类型的指针,但是对于字符串,想要创建指向数据类型char的指针。变量的声明应该像这样:
char *result;
这创建称为result的变量,可以使用它包含指向字符串的指针。
默认情况下,C程序假设函数的返回值是整数值。必须通知编译器这个函数将返回字符串的
指针。创建函数调用的原型(prototype)将完成这个任务。
原型在使用函数之前定义函数格式,以便C编译器直到如何处理函数调用。原型定义函数
需要的输入值的数据类型,还有返回值的数据类型。不需定义特定的值,只定义需要的数据类
型,原型的例子如下:
char *functionl( int, int );
函数模板后面有分号,没有这个分号,编译器就会假设你的代码中定义的是整个函数,并
且会生成错误消息。如果函数没有使用任何输入值,仍然必须指定数据类型void:
char *function( void );
在源代码中,原型必须出现在main()段之前,并且通常放置在所有#include和#define语句
之后。
.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
#include <stdio.h>
char *cpuidfunc( void );
int main()
{
char *spValue;
spValue = cpuidfunc();
printf( "The CPUID is: ‘%s’\n", spValue );
return 0;
}
3、使用浮点返回值
整数和字符串返回值都使用EAX寄存器把值从汇编语言函数返回到发出调用的C程序。如果
是浮点值,情况稍微有些不同。
C样式的函数不使用EAX寄存器,而是使用ST(0)寄存器在函数之间交换浮点值。函数把返
回值存放到FPU堆栈中,然后调用程序负责把返回值弹出堆栈并且把它赋值给变量。
因为把浮点值存放到FPU堆栈中时,总会把浮点值转换为扩展双精度浮点值,所以原始值
是什么浮点数据类型,以及C程序使用什么浮点数据类型包含结果值不重要。由FPU完成适当的
转换。
C和C++程序使用两种数据类型表示浮点值:
- float:表示单精度浮点值
- double:表示双精度浮点值
以上每一种类型都可以用于FPU堆栈获得结果值。汇编语言函数使用什么精度把值存放打
到FPU堆栈中不重要。
在C程序中使用返回浮点值的函数时,必须定义函数原型。和字符串返回值的原型相同的原
则在这里也适用:
float function1( float, float, int );
如果变量使用双精度浮点值,就必须使用返回类型double:
double function1( double, int );
.section .text
.type areafunc, @function
.globl areafunc
areafunc:
pushl %ebp
movl %esp, %ebp
fldpi
filds 8(%ebp)
fmul %st(0), %st(0)
fmul %st(1), %st(0)
movl %ebp, %esp
popl %ebp
ret
#include <stdio.h>
float arefunc( int );
int main()
{
int radius = 10;
float result;
result = areafunc( radius );
printf( "The result is %f\n", result );
result = areafunc( 2 );
printf( "The result is %f\n", result );
return 0;
}
4、使用多个输入值
使用多个输入值时,必须谨慎地注意按照什么顺序把它们传递给函数。输入值按
照C函数中列出从左到右的顺序存放。因此,函数
int i = 10;
int j = 20;
result = function( i, j );
第二个输入值值j先存放在堆栈中,接下来存放第一个输入值i。记住堆栈是向下增长的。在函
数中使用位置8(%ebp)引用第一个输入值,使用位置12(%ebp)引用第二个输入值。
.section .text
.globl greater
greater:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
movl 12(%ebp), %ecx
cmpl %ecx, %eax
jge end
movl %ecx, %eax
end:
movl %ebp, %esp
popl %ebp
ret
#include <stdio.h>
int main()
{
int i = 10;
int j = 20;
int k = greater( i, j );
printf( "The larger value is %d\n", k );
return 0;
}
5、使用混合数据类型的输入值
使用混合类型的输入值时会遇到两个问题:
- 调用函数可能按照错误的顺序把值存放到堆栈中
- 汇编函数可能按照错误的顺序从堆栈读取值
必须谨慎处理,以确保调用程序正确地把输入值存放到函数中,并且汇编语言函
数按照正确的顺序从堆栈读取输入值。
1)按照正确的顺序存放输入值
如果在处理使用不同数据长度的数据类型,就必须确保调用程序按照汇编语言函数读取输
入值的顺序把输入值存放到堆栈中。如果某些值是4字节整数值,而其他值是8字节双精度浮点
值,那么如果函数没有正确读取它们,就会发生错误。
.section .text
.type testfunc, @function
.globl testfunc
testfunc:
pushl %ebp
movl %esp, %ebp
fldl 8(%ebp)
fimul 16(%ebp)
movl %ebp, %esp
popl %ebp
ret
testfunc.s函数假设第一个输入值是8字节的双精度浮点值,并且把它加载到FPU堆栈
中。然后它假设下一个输入值位于距离第一个值8字节的位置,并且是4字节整数值。这个值和
FPU堆栈中第一个值相乘。结果返回到FPU堆栈的寄存器ST(0)中。
下面是错误顺序传递输入值的程序。
#include <stdio.h>
double testfunc( int, double );
int main()
{
int data1 = 10;
double data2 = 3.14159;
double result;
result = testfunc( data1, data2 );
printf( "The bad result is %g\n", result );
return 0;
}
下面是正确的程序:
#include <stdio.h>
double testfunc( double, int );
int main()
{
int data2 = 10;
double data1 = 3.14159;
double result;
result = testfunc( data1, data2 );
printf( "The bad result is %g\n", result );
return 0;
}
2)按照正确的顺序读取输入值
除了确保调用程序按照正确的顺序存放输入值之外,还必须确保汇编函数正确地读入值。
.section .text
.type fpmathfunc, @function
.globl fpmathfunc
fpmathfunc:
pushl %ebp
movl %esp, %ebp
flds 8(%ebp)
fidiv 12(%ebp)
flds 16(%ebp)
flds 20(%ebp)
fmul %st(1), %st(0)
fadd %st(2), %st(0)
flds 24(%ebp)
fimul 28(%ebp)
flds 32(%ebp)
flds 36(%ebp)
fdivrp
fsubr %st(1), %st(0)
fdivr %st(2), %st(0)
movl %ebp, %esp
popl %ebp
ret
#include <stdio.h>
int main()
{
float value1 = 43.65;
int value2 = 22;
float value3 = 76.34;
float value4 = 3.1;
float value5 = 12.43;
int value6 = 6;
float value7 = 140.2;
float value8 = 94.21;
float result;
result = fpmathfunc( value1, value2, value3, value4,
value5, value6, value7, value8 );
printf( "The final result is %f\n", result );
return 0;
}
四、在C++程序中使汇编函数
C++程序下使用汇编只有一处重要的区别。
默认情况下,C++程序假设C++程序中使用的所有函数都使用C++样式的命名和调用约定。但
是,程序中使用的汇编语言函数使用C语言的调用约定。必须通知编译器使用的哪些函数是C函
数。这是通过extern语句完成的。
extern "C"
{
int square( int );
float areafunc( int );
char *cpuidfunc();
}
下面程序演示在C++程序中使用汇编代码:
#include <iostream>
using namespace std;
extern "C"
{
int square( int );
float areafunc( int );
char *cpuidfunc();
}
int main()
{
int radius = 10;
int radsquare = square( radius );
cout << "The radius squared is" << radsquare << endl;
float result;
result = areafunc( radius );
cout << "The are is" << result << endl;
cout << "The CPUID is" << cpuidfunc() << endl;
return 0;
}
五、创建静态库
1、什么是静态库
存档文件可以用于编译任何使用存档文件中包含的任何函数的程序。这种存档文
件被称为库文件(library file)。
库文件包含很多函数的目标文件。在库文件中,经常按照应用程序类型或者函数类型把函数
分组在一起。单一应用程序项目可以使用多个库文件。
这种类型的库文件被称为静态的,因为库文件中包含的目标代码被编译器编译到主程序中。
函数的目标代码被编译到可执行代码中之后,可执行程序的运行就不需要库文件了。但是,这意
味着程序的每个拷贝都在其中包含函数的代码。
2、ar命令
在Linux下,ar命令用于创建静态库文件。ar命令创建可供编译器读取的函数目标
文件的存档文件。
可以使用一个或者多个修饰符修改基本选项。
3、创建静态库文件
Linux下的命名规定如下:
libx.a
其中x是库的名称,扩展名a标识这个文件是静态库文件。
下面的命令创建本章中迄今位置介绍过的汇编语言函数的存档文件:
使用ranlib程序创建库的索引。索引存放在库文件内部。
4、编译静态库
使用库来编译程序不会影响生成的可执行文件的长度。
六、使用共享库
1、什么是共享库
当应用程序和静态库一起编译时,函数代码被编译到了应用程序中。这就是说应
用程序所需的所有代码都在可执行程序文件中。
这种方法有下面的缺陷:
- 如果在函数代码中改动某些内容内容,使用此函数的每个应用程序都必须使用新的版本重新编译
- 使用相同函数的多个程序都必须包含相同的代码。这使小型的应用程序比它所需长度的要长,因为它必须包含用到的每个函数的所有代码。
- 运行在系统上的多个程序可能会使用相同函数,这意味着多次把相同的函数加载到内存中。
共享库试图解决这些问题。包含函数目标代码的单独文件被加载到操作系统的同样区域中。
当应用程序需要访问共享库中的函数时,操作系统自动地把函数代码加载到内存中,并且允许应
用程序访问它。
如果另一个应用程序也需要使用此函数代码,操作系统允许它访问已经被加载到内存的相同
的函数代码。只有函数的一个拷贝被加载到了内存中,并且使用此函数代码的每个独立程序不需
要把它加载到它们的内存空间中,或者它们的可执行文件中。
如果需要对函数进行任何改动,唯一需要更新的文件只有单一的共享库文件。使用共享库的
每个程序都会自动使用函数的新版本。当然,当函数的改动改变它原来的行为时会出现问题。这
就是Windows环境中导致使用共享库的应用程序被破坏的原因。(Linux提供一种创建版本号的方
法,调用程序可以比较版本号)。
2、创建共享库
使用gcc编译器从目标文件创建共享库。在创建共享库之前,必须使用as对汇编器语言函数
进行汇编。Linux具有用于共享库的命名约定:
libx.so
其中x是库的名称,扩展名.so表明这是共享库。
用于创建共享库的gcc命令行选项是-shared选项:
3、编译共享库
虽然共享库文件不被编译到C程序中,但是编译器仍必须直到如何访问函数。使用
-l选项加上共享库名称(减去lib部分和.so扩展名),在编译命令行上导入共享库。在
能够使用共享库之前,必须使用-L通知编译器去哪里查找它。和-L选项一起使用的参
数是编译器查找共享库的其他位置,除了操作系统中定义的位置外。如果共享库位于
和程序文件相同的目录中,可以使用句号表示相同的目录。
这使编译命令行如下:
编译器创建不包含函数代码的可执行文件inttest。为了验证这点,可以使用objdump看到
包含下面这一行:
为了查看可执行文件依赖什么共享库,可以使用ldd命令:
libchap14.so这个库并没有在系统中找到它,如果试图运行可执行文件就会发生这样的结果:
4、运行使用共享库的程序
动态加载器必须知道如何访问共享库libchap14.so。有两种方式通知它文件位于什
么位置:
- LD_LIBRARY_PATH环境变量
- /etc/ld.so.conf文件
1)LD_LIBRARY_PATH环境变量
LD_LIBRARY_PATH环境变量是系统上的任何用户为当前进程的动态加载器添加
路径的简单方式。它包含一个路径清单(以冒号分隔),动态加载器应该在ld.so.conf
文件中列出的路径之外的这些位置查找库文件。使用LD_LIBRARY_PATH环境变量
不需要任何专门权限,只需设置它:
2)/etc/ld.so.conf文件
文件ld.so.conf位于/etc目录中,它保存动态加载器在那些目录中查找库的目录清
单:
可以把新目录添加到文件ld.so.conf中,然后以root身份运行ldconfig命令以更新ld.so.cache,
动态链接器将使用它。
七、调试汇编函数
1、调试C程序
使用gdb调试器调试C程序与使用它调试汇编语言程序非常类似。
2、调试汇编函数
把调试信息汇编到其中,以创建两个库:一个包含不带调试信息的汇编语言函数
的目标文件,一个包含带有调试信息的目标文件。这可以生成程序的开发型版本和产
品型两个版本。