爱技术 & 爱分享
爱蛋蛋 & 爱生活

内联汇编

创建内联汇编代码和创建汇编函数没有太大区别,除了这是在C或者C++程序内完成的之外。

GNU的C编译器使用asm关键字指出使用汇编语言编写的源代码段落。asm段的基本格式如下:

asm("assembly code");

包含在括号中的汇编代码必须按照特定的格式:

  • 指令必须括在引号里。
  • 如果包含的指令超过一条,那么必须使用新行字符分隔汇编语言代码的每一行。通常,还包含制表符帮助缩进汇编语言代码,使代码行更容易阅读。

需要第二个规则是因为编译器逐字地取得asm段中的汇编代码,并且把它们放在为程序生成的汇编代码中。每条汇编语言指令都必须在单独的一行中,因此需要包含新行字符。

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

基本的内联汇编代码可以利用应用程序中定义的全局C变量。这里要记住的是“全局”这个词。只有全局定义的变量才能在基本的内联汇编代码内使用。通过C程序中使用的相同名称引用这种变量。

在一般的C或者C++应用程序中,编译器也许会试图优化生成的汇编代码以提高性能。通常这是通过:消除不使用的函数,在不同时使用的值之间共享寄存器,以及重新编排代码以便实现更好的程序流程这样的方式完成的。

对于内联汇编函数来说, 有时候优化并不是好事情。编译器也可能查看内联代码并且试图优化它,这可能会产生不希望的后果。

如果希望编译器不处理手动编码的内联汇编函数,可以把volatile修饰符放在asm语句中表示不希望优化这个代码段。使用volatile修饰符的asm语句的格式如下:

asm volatile ("assembly code");

这个语句内的汇编代码使用不带volatile修饰符时使用的标准规则。volatile修饰符的添加不改变在内联汇编代码中存储和获得寄存器值的要求。

ANSI C规范把关键字asm用于其他用途,不能将它用于内联汇编语句。如果使用ANSI C约定编写代码,你必须使用关键字_asm_替换一般的关键字asm

语句中的汇编代码段不必改动,和使用关键字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");

关键字_asm_也可以使用修饰符__volatile__进行修饰。

基本的asm格式提供创建汇编代码的简单方式,但是有其局限性。首先,所有输入值和输出值都必须使用C程序的全局变量。其次, 必须极为注意在内联汇编代码中不去改变任何寄存器的值。GNU编译器提供asm段的扩展格式来帮助解决这些问题。扩展格式提供附加的选项,可以更加精确地控制在C或者C++语言程序中如何生成内联汇编语言代码。

扩展asm格式

因为扩展asm格式提供附加的特性,所以它们必须采用新的格式。asm扩展版本的格式如下:

asm ("assembly code" : output locations : input operands : chagned registers);

这种格式由4个部分构成,使用冒号分隔:

  • 汇编代码:使用和基本asm格式相同的语法的内联汇编代码
  • 输出位置:包含内联汇编代码的输出值的寄存器和内存位置的列表
  • 输入操作数:包含内联汇编代码的输入值的寄存器和内存位置的列表
  • 改动的寄存器:内联代码改变的任何其他寄存器的列表

在扩展asm格式中,并不是所有这些部分都必须出现。如果汇编代码不生成输出值,这个部分就必须为空,但是必须使用两个冒号把汇编代码和输入操作数分隔开。如果内联汇编代码不改动寄存器的值,那么可以忽略最后的冒号。

在扩展格式中,可以从寄存器和内存位置给输入值和输出值赋值。输入值和输出值列表的格式是:

"constraint" (variable)

其中variable 是程序中声明的C 变量。在扩展asm 格式中,局部和全局变量都可以使用。

constraint定义把变量存放到哪里(对于输入值)或者从哪里传送变址(对于输出值)。使用它定义把变量存放在寄存器中还是内存位置中。

约束是单一字符的代码。约束代码如下表所示。

约束 描述
a 使用%eax 、%ax或者%al寄存器
b 使用%ebx 、%bx或者%bl寄存器
c 使用%ecx 、%cx或者%cl寄存器
d 使用%edx 、%dx或者%dl寄存器
S 使用%esi或者%si寄存器
D 使用%edi或者%di寄存器
r 使用任何可用的通用寄存器
q 使用%eax 、%ebx 、%ecx 或者%edx寄存器之一
A 对于64位值使用%eax和%edx寄存器
f 使用浮点寄存器
t 使用第一个(顶部的)浮点寄存器
u 使用第二个浮点寄存器
m 使用变量的内存位置
o 使用偏移内存位置
V 只使用直接内存位置
i 使用立即整数值
n 使用值已知的立即整数值
g 使用任何可用的寄存器或者内存位置

除了这些约束之外,输出值还包含一个约束修饰符,它指示编译器如何处理输出值。可以使用的输出修饰符如下表所示。

输出修饰符 描述
+ 可以读取和写入操作数
= 只能写入操作数
% 如果必要,操作数可以和下一个操作数切换
& 在内联函数完成之前,可以删除或者重新使用操作数

在扩展asm格式中,为了在汇编代码中引用寄存器,必须使用两个百分号符号,而不是一个。

扩展asm格式提供了占位符(placeholder) ,可以在内联汇编代码中使用它引用输入值和输出值。这样可以在对于编译器方便的任何寄存器或者内存位置中声明输入和输出值。

占位符是前面加上百分号符号的数字。按照内联汇编代码中列出的每个输入值和输出值在列表中的位置,每个值被赋予一个从零开始的数字。然后就可以在汇编代码中使用占位符表示值。

例如,下面的内联代码:

asm ( "assembly code"
: "=r" (result)
: "r" (data1), "r" (data2));

将生成如下的占位符:

  • %0 将表示包含变量值result的寄存器。
  • %1 将表示包含变量值data1的寄存器。
  • %2 将表示包含变量值data2的寄存器。

注意,占位符提供在内联汇编代码中利用寄存器和内存位置的方法。汇编代码中使用占位符只作为原始的数据类型:

imull %1, %2
movl %2, %0

记住,必须把输入值和输出值声明为内联代码中汇编指令需要的正确的存储元素(寄存器或者内存)。在这个例子中,两个输入值都必须加载到寄存器中以供IMULL指令使用。

如果处理很多输入值和输出值,数字型的占位符很快就会变得混乱。为了使条理清晰,GNU编译器(从版本3.1 开始)允许声明替换的名称作为占位符。

替换的名称在声明输入值和输出值的段中定义。格式如下:

%[name]"constraint"(variable)

定义的值name成为内联汇编代码中变量的新的占位符标识符,如下面的例子所示:

asm ("imull %[valuel], %[value2]"
: [value2] "=r" (data2) 、
: [valuel] "r"(datal), "o"(data2));

使用替换的占位符名称的方式和使用普通的占位符相同。

编译器假设输入值和输出值使用的寄存器会被改动,并且相应地做出处理。程序员不需要在改动的寄存器列表中包含这些值,如果这样做了,就会产生错误消息。

注意改动的寄存器列表中的寄存器使用完整的寄存器名称,而不像输入和输出寄存器定义那样仅仅是单一字母。在寄存器名称前面使用百分号符号是可选的。

改动的寄存器列表的正确使用方法是,如果内联汇编代码使用了没有被初始地声明为输入值或者输出值的任何其他寄存器,则要通知编译器。编译器必须知道这些寄存器,以便避免使用它们。

改动的寄存器列表有个奇怪的地方:如果在内联汇编代码之内使用了没有在输入值或者输出值中定义的任何内存位置,那它必须被标记为被破坏的。在改动的寄存器列表中使用”memory” 这个词通知编译器这个内存位置在内联汇编代码中被改动。

因为FPU以堆栈方式使用寄存器, 所以在编写内联汇编语言代码的过程中使用浮点值就有一
点区别。必须更加留意内联代码处理FPU寄存器的方式。

处理FPU寄存器堆栈的约束有3 个:

  • f 引用任何可用的浮点寄存器
  • t 引用顶部的浮点寄存器
  • u 引用第二个浮点寄存器

浮点值

从FPU获得输出值的时候,不能使用约束f; 必须声明约束t或者u来指定输出值所在的FPU寄
存器, 就像下面的例子:

asm ("fsincos"
: "=t"(cosine), "=u"(sine)
: "0"(radian));

FSINCOS指令把输出值存放在FPU堆栈的前两个寄存器中。必须为正确的输出值指定正确的寄存器。因为输入值也必须存放在ST(O)寄存器中,所以它和第一个输出值使用相同的寄存器,并且使用占位符声明它。

跳转处理

内联汇编语言代码也可以包含定义其中位置的标签。可以实现一般的汇编条件分支和无条
件分支,跳转到定义的标签。

在内联汇编代码中使用标签时有两个限制。第一个限制是只能跳转到相同的asm段内的标签。
不能从一个asm段跳转到另一个asm段中的标签。

第二个限制更加复杂一些。多个asm段,不能使用相同的标签,否则会因为标签的重复使
用而导致错误消息。还有,如果试图整合使用C关键字(比如函数名称或者全局变量)的标签,
也会导致错误。

这个问题有两个解决方案。最简单的解决方案是在不同的asm段中使用不同的标签。如果
手工编码每个asm段,这是可行的方案。

如果使用相同的asm段就不能改变内联汇编代码中的标签。这时的解决方案是使用局部标签。条件分支和无条件分支都允许指定一个数字加上方向标志作为标签, 方向标志指出处理器应该向哪个方向查找数字型标签。第一个遇到的标签会被采用。

上述笔记内容学习自 AT&T Professional Assembly Language — Chapter XIII

赞(1)
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。墨影 » 内联汇编