曲径通幽论坛

 找回密码
 立即注册
搜索
查看: 6859|回复: 8
打印 上一主题 下一主题

内联汇编

[复制链接]

4917

主题

5879

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34382
跳转到指定楼层
楼主
发表于 2010-8-23 14:37:02 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
基本格式:
asm( "assembly code" );

简单举例:
书写格式 1:
asm ("movl $1, %eax\n\tmovl $0, %ebx\n\tint $0x80");
上面使用转义字符 \n 和 \t 。这样看起来很凌乱,改成如下格式则较为清晰:
asm ( "movl $1, %eax\n\t"
          "movl $0, %ebx\n\t"
          "int $0x80");
上面将指令放在单独的行中。这样做,每条指令必须括在引号里。这种格式容易阅读,在调试时也感觉轻松。asm 段可以放在 C 或 C++ 源代码中的任何地方。

下面程序演示 asm 段在实际程序中是什么样子的:
#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);
}
使用 gcc 的 -S 选项生成反汇编代码:
    .file    "asm.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
# 9 "asm.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 4.4.3-4ubuntu5) 4.4.3"
    .section    .note.GNU-stack,"",@progbits
由上可见,由 #APP 和 #NO_APP 符号标识的段落正是 asm 段指定的内联汇编代码。

4917

主题

5879

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34382
沙发
 楼主| 发表于 2010-8-23 17:19:29 | 只看该作者

使用 volatile

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

但对于内联汇编函数来说,优化不总是好事,有时可能会产生不希望的结果。

如果希望编译器不处理手动编码的内联汇编函数,可以明确说明。说明的方法是将 volatile 修饰符放在 asm 语句中表示不希望优化这个代码段。使用格式如下:
asm volatitle ("assembly code");
volatile 修饰符的添加不改变内联汇编代码中存储和获得寄存器值的要求。

4917

主题

5879

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34382
板凳
 楼主| 发表于 2010-8-23 18:07:33 | 只看该作者

使用 __asm__ 替换 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");
另外,关键字也可以使用修饰符 __volatile__ 进行修饰。

4917

主题

5879

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34382
地板
 楼主| 发表于 2010-8-23 18:55:23 | 只看该作者

扩展 asm

基本 asm 虽然使用简单,但有局限性:
1. 所有输入参数和返回值都必须使用 C 程序的全局变量。
2. 必须十分小心在内联汇编代码中不去改变任何寄存器的值。

所以,GNU 编译器提供了 asm 段的扩展格式来帮助解决这些问题。扩展格式选项可以更加精确地控制在 C 或者 C++ 中如何生成内联汇编代码。

1. 扩展 asm 格式
asm 扩展版本的格式如下:
asm ("汇编代码" : 输出位置 : 输入操作数 : 改动的寄存器
格式中 4 个部分使用冒号隔开,它们的含义分别是:
      汇编代码 : 使用和基本 asm 格式相同的语法的内联汇编代码。
      输出位置 : 包含内联汇编代码的输出值的寄存器和内存位置的列表。
      输入操作数 : 包含内联汇编代码的输入值的寄存器和内存位置的列表。
      改动的寄存器 : 内联代码改变的任何其他寄存器列表
上面 4 个部分,并不要求每次都要全部出现。比如,
如果汇编代码不生成输出值,这个部位就必须为空,但是需要用两个冒号把汇编代码和输入操作数分隔开。
如果内联汇编代码不改动寄存器的值,那么可以忽略最后的冒号。

2. 指定输入值和输出值
基本 asm 格式的汇编代码里必须通过 C 全局变量来整合输入值和输出值。而在扩展格式中,可以从寄存器和内存位置给输入值和输出值赋值。输入值和输出值列表的格式是:
"constraint" (variable)
上面,variable 是程序中声明的 C 变量。在扩展 asm 中,可以使用 C 的全局变量和局部变量。constraint 定义把变量存放到哪里(对于输入值)或者从哪里传送变量(对于输出值)。使用它定义把变量存放在寄存器中还是在内存里。

约束(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
使用任何可用的寄存器或者内存位置
I
使用常数0~31
J
使用常数0~63
K
使用常数0~255
L
使用常数0~65535
M
使用常数0~3
N
使用1字节常数(0~255)
O
使用常数0~31
cc
如果汇编代码会改变条件码寄存器,则最好添加此项(在第三个冒号后的那一栏,并不是对所有CPU有效)
memory
用在损坏部分中,表示内存被修改了

除了上面的约束外,输出值还包含了一个约束修饰符,它指示编译器如何处理输出值,可以使用的输出修饰符如下所示:
输出修饰符
描述
+
可以读取和写入操作数
=
只能写入操作数
%
如果必要,操作数可以和下一个操作数切换
&
确保输入和输出分配到不同的寄存器中

下面是一个简单举例:
asm ("assembly code" : "=a"(result) : "d"(data1), "c"(data2));
上面,将 C 变量 data1 放入 EDX 寄存器中,将 data2 存放到 ECX 寄存器中。内联汇编代码结果将存放在 EAX 寄存器中,然后传递给变量 result 。

3. 使用寄存器
在扩展 asm 中,在汇编代码里引用寄存器,必须使用两个百分号(这用来帮助GCC区分操作数和寄存器,操作数只需要一个%前缀),而不是一个。如下所示:
#include <stdio.h>

int main()
{
     int data1 = 10;
     int data2 = 20;
     int result;

     asm (    "imull %%edx, %%ecx\\n\\t"
                   "movl %%ecx, %%eax"
                   : "=a"(result)
                   : "d"(data1), "c"(data2));

     printf ("The result is %d\\n", result);
     return (0);
}
用 gcc 的 -S 选项产生汇编代码:
movl    $10, 28(%esp)  
         movl    $20, 24(%esp)
         movl    28(%esp), %eax
         movl    24(%esp), %ecx
         movl    %eax, %edx
#APP
# 9 "reg.c" 1
         imull %edx, %ecx
         movl %ecx, %eax
# 0 "" 2
#NO_APP
         movl    %eax, 20(%esp)
在上面的汇编代码中。编译器将 data1 和 data2 的值先放入堆栈中。然后按照内联汇编代码的要求,再把这两个值分别加载到 EDX 和 ECX 中,接着两个寄存器值相乘,结果放入 EAX 中(movl %ecx, %eax),最后再将这个结果存往堆栈中的变量位置 result 处。

一个情况是,并不总是要在内联汇编代码段中指出输出值,这是因为一些汇编指令的输入值部分就已经包含了输出值,如字符串复制就是这样的例子:
#include <stdio.h>

int main()
{
     char input[30] = {"This is a test message.\\n"};
     char output[30];
     int length = 25;

     asm volatile (    "cld\\n\\t"
             "rep movsb"
             :
             : "S"(input), "D"(output), "c"(length));
     
     printf ("%s", output);
     return (0);
}
在上面这个例子中,输出值已经在 EDI 寄存器中指出,即输出值已经被定义为输入值之一。复制字符串的长度放在 ECX 中。
这里,需要注意的是,由于没有专门定义输出值,所以要用上 volatile 关键字!否则,编译器可能会认为这个 asm 段没有必要(因为没有输出值)而将其优化掉(被删除)!

4917

主题

5879

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34382
5#
 楼主| 发表于 2010-8-23 17:05:35 | 只看该作者

使用全局 C 变量

仅仅实现汇编代码本身并不能完成很多任务。为了完成实际工作,还需要将数据传进和传出(传进参数与返回结果)。

基本内联汇编代码可以利用应用程序中定义的 C 全局变量。注意,只有全局定义的变量才能在基本的内联汇编代码内使用。下面程序演示内联汇编引用 C 中相同的全局变量。
#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);
}
反汇编后:
.file    "global.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
# 9 "global.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 4.4.3-4ubuntu5) 4.4.3"
     .section    .note.GNU-stack,"",@progbits
在 .data 段中,声明了 a 和 b 变量并赋值。由于 result 变量在 C 中并未赋初值,所以被声明为 .comm 。.comm 可以使用第3个参数。它代表符号需要对齐的边界基准(例如, 边界基准为 16 时意味着符号存放地址的最低4位应该是零)。第3个参数表达式结果必须是纯粹的数字,而且一定是2的幂。
需要注意的是,在内联汇编代码段,开始时使用了 PUSHA 指令,结尾时使用了 POPA 指令。这一工作很重要,因为编译器可能会使用这些寄存器存放其他的值,如果在 asm 段中修改它们,可能会产生不可预料的结果。

4917

主题

5879

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34382
6#
 楼主| 发表于 2010-8-29 18:05:12 | 只看该作者

占位符(placeholder)

在内联汇编代码中,如果输入参数比较多,在指令中使用寄存器会显得繁琐。因此,扩展 asm 格式提供了占位符(placeholder)以解决这个问题。

占位符的表达方式是百分号+数字,如 %0, %1 等。它的定义是:
根据内联汇编代码中列出的每个输入值和输出值在列表中的位置,每个值被赋予一个从 0 开始的数字。
例如如下格式的汇编代码
asm ("assembly code"
         : "=r"(result)
         : "r"(data1), "r"(data2));
会生成下面的占位符:
%0 表示包含变量 result 值的寄存器;
%1 表示包含变量 data1 值的寄存器;
%2 表示包含变量 data2 值的寄存器。

测试程序:
#include <stdio.h>

int main()
{
     int data1 = 10;
     int data2 = 20;
     int result;

     asm (    "imull %1, %2\\n\\t"
              "movl %2, %0"
         : "=r"(result)
         : "r"(data1), "r"(data2));
     
     printf ("The result is %d\\n", result);
     return (0);
}
查看内联汇编处的反汇编代码:
    movl    $10, 28(%esp)
    movl    $20, 24(%esp)
    movl    28(%esp), %eax
    movl    24(%esp), %edx
#APP
# 9 "regtest2.c" 1
    imull %eax, %edx
    movl %edx, %eax
# 0 "" 2
#NO_APP
从上面的反汇编代码可看到,有一句 movl %eax, %eax ,显然这条语句没有必要。造成这个原因,是因为程序中定义了一个“不必要“的变量 result ,由于约束条件为 r,所以在没有其他不良的影响下,编译器仍然用 EAX 来加载这个 result 值。如果没有特殊需求,我们可以使用 data2 来当作输出值,这样就可以少去定义 result 这个变量(这也就是“没必要“的解释)。

在输入值和输出值共享相同的 C 变量时,可以指定使用占位符作为约束值。如下代码段所示:
asm ("imull %1, %0"
         : "=r"(data2)
         : "r"(data1), "0"(data2));
上面代码中,0标记告诉编译器用第一个命名的寄存器来存放 data2 的值。在这里,第 2 行代码("=r"(data2))为输入变量 data2 分配了一个寄存器,这个寄存器就是第一个被命名的寄存器,所以第 3 行指令里的 data2 就用第 2 行中分配的哪个寄存器(因为它们是同一个变量)。
测试代码:
#include <stdio.h>

int main()
{
     int data1 = 10;
     int data2 = 20;

     asm (    "imull %1, %0"
         : "=r"(data2)
         : "r"(data1), "0"(data2));
     
     printf ("The result is %d\\n", data2);
     return (0);
}

替换占位符
如果需要处理很多输入输出值,数字型的占位符会很快变得混乱。现在 GNU 编译器允许声明替换的名称作为占位符,这样会使得程序代码变得条例清晰。
替换名定义如下:
%[name]约束条件(variable)
使用替换的占位符名称的方式和使用普通的占位符相同:
#include <stdio.h>

int main()
{
     int data1 = 10;
     int data2 = 20;

     asm (    "imull %[value1], %[value2]"
         : [value2] "=r"(data2)
         : [value1] "r"(data1), "0"(data2));

     printf ("The result is %d\\n", data2);

     return (0);
}

4917

主题

5879

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34382
7#
 楼主| 发表于 2010-8-29 22:56:41 | 只看该作者

改动的寄存器列表

在扩展 asm 的最后一个冒号栏里,是改动的寄存器列表。改动的寄存器列表就是说,在这个列表里的寄存器是要被改动的,它可以在输入值和输出值寄存器之间的运算,但编译器不会用它来存储输入输出值。

下面是一个错误的用法:
#include <stdio.h>

int main()
{
     int data1 = 10;
     int result = 20;
     
     asm (    "addl %1, %0"
         : "=d"(result)
         : "c"(data1), "0"(result)
         : "%ecx", "edx");

     printf ("The result is %d\\n", result);
     
     return (0);
}
编译时出错提示:
$ gcc -g badreg.c -o badreg
badreg.c: In function ‘main’:
badreg.c:8: error: can't find a register in class ‘DREG’ while reloading ‘asm’
badreg.c:8: error: ‘asm’ operand has impossible constraints
在上面的代码中,改变寄存器列表里寄存器是以全名称出现的,而不能只是像输入输出寄存器定义那样只用一个字母来表示,但是寄存器前面的百分号可有可无。

出错的原因是,编译器已经直到 EDX 使用作输出值寄存器,而改动寄存器列表里再包含了它。也就是说,如果你将一个寄存器用作输入输出值寄存器,那么就不能将其包含在改动寄存器列表里,反过来也是一样的。

下面是正确的函数:
#include <stdio.h>

int main()
{
     int data1 = 10;
     int result = 20;
     
     asm (    "movl %1, %%eax\\n\\t"
         "addl %%eax, %0"
         : "=d"(result)
         : "c"(data1), "0"(result)
         : "%eax");

     printf ("The result is %d\\n", result);
     
     return (0);
}
使用 -S 选项生成反汇编代码:
    movl    $10, 28(%esp)
    movl    $20, 24(%esp)
    movl    28(%esp), %ecx
    movl    24(%esp), %eax
    movl    %eax, %edx
#APP
# 8 "okreg.c" 1
    movl %ecx, %eax
    addl %eax, %edx
# 0 "" 2
#NO_APP
上面,在改动寄存器列表里包含了 EAX 寄存器。这样,编译器就不会再用 EAX 来装载输入输出值,EAX 被用来参与变量间的运算。

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

4917

主题

5879

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34382
8#
 楼主| 发表于 2010-8-29 23:17:03 | 只看该作者

使用内存位置

虽然在内联汇编代码中使用寄存器比较快,但也可以使用 C 变量的内存位置。约束 m 用来引用输入值和输出值的内存位置。需要注意的是,对于要求使用寄存器的汇编指令,仍然必须使用寄存器。下面是示例程序:
#include <stdio.h>

int main()
{
     int dividend = 20;
     int divisor = 5;
     int result;

     asm (    "divb %2\\n\\t"
         "movl %%eax, %0"
         : "=m"(result)
         : "a"(dividend), "m"(divisor));

     printf ("The result is %d\\n", result);
     return (0);
}
上面的内联汇编代码中,被除数加载到 EAX 中,DIV 指令需要它;除数则放在内存位置中,作为输出值。生成的汇编代码如下:
    movl    $20, 28(%esp)
    movl    $5, 24(%esp)
    movl    28(%esp), %eax
#APP
# 9 "mem.c" 1
    divb 24(%esp)
    movl %eax, 20(%esp)
# 0 "" 2
#NO_APP
从上面的汇编代码可看到,当除法结束后,结果被送到了堆栈("=m"(result)),而不是在寄存器中(EAX的作用是中间寄存器)。

4917

主题

5879

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34382
9#
 楼主| 发表于 2011-3-9 19:02:09 | 只看该作者

交换指令中的 %b0

在内联汇编中,可能还会看到如 %b0 这种形式。它表示取 %0 这个操作数的一个字节。下面用交换指令来演示这种形式。


代码:

[C++] 纯文本查看 复制代码
#include <stdio.h>
void xchange (int src, int des)
{
        __asm__ ("xchgb %b0, %b1"
                :"=b" (des), "=a"(src)
                :"a" (src), "b" (des)
                :"memory");


        printf ("src is : 0x%x\\n", src);
        printf ("des is : 0x%x\\n", des);
}
int main(void)
{
        int src = 0x12345678;
        int des = 0x87654321;


        xchange (src, des);


        return (0);
}


运行输出:
[beyes@SLinux assembly]$ ./xchg
src is : 0x12345621
des is : 0x87654378
代码中,xchgb %b0, %b1 表示交换 %0 和 %1 中的一个字节,前面的 b 是必须的;如果是交换字,那么可以用 w 。
如果改成 xchgb %b0, %1 或 ,那么都会提示以下这样的错误(缺少相应的匹配前缀):
Error: suffix or operands invalid for `xchg'
这是因为,如果两个操作数都为寄存器的时候,要明确指定匹配,即字节对字节(如 al 对 bl)。如果是寄存器对内存,那就没有这种问题,如下面的例子:

__asm__ ("xchgb %b0, %1"
                :"=q" (des)
                :"m" (src), "0" (des)
                :"memory");
上面,%0 是寄存器,%1 是内存,所以这里只要指定寄存器为一个字节(%b0),而内存 %1 不需要明确指出来,编译器自己能够察觉,当然对内存明确指定 (%b1),那自然也是不会错的。如果两个都不指定,编译虽然不会发生错误,但是会发生警告,如:
[beyes@SLinux assembly]$ gcc xchg.c -o xchg
xchg.c: Assembler messages:
xchg.c:5: Warning: using `%al' instead of `%eax' due to `b' suffix

总结一下:
1. 如果两个是寄存器的,那么就同时使用 b 前缀;
2. 如果一个是寄存器,一个是内存,那么就为寄存器指定 b 前缀即可,内存无需指定。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|曲径通幽 ( 琼ICP备11001422号-1|公安备案:46900502000207 )

GMT+8, 2024-4-29 03:25 , Processed in 0.092006 second(s), 22 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表