GCC内联汇编#

@source https://blog.csdn.net/hans774882968/article/details/127141703

https://bbs.kanxue.com/thread-279604.htm

https://blog.csdn.net/abel_big_xu/article/details/117927674

https://forum.butian.net/share/2930

https://blog.csdn.net/m0_46296905/article/details/117336574

基本格式

asm volatile(
     "汇编语句"
     : 输出部分
     : 输入部分
     : 会被修改的部分(clobbered register)
);

汇编语句必填,其它三部分可选

#include <stdio.h>
int main()
{
	int src = 1;
	int dst;   
	asm volatile(
        "movl %1, %0\n\t"
        "add $1, %0\n\t"
        "add $3, %0\n\t"
        : "=r" (dst) 
        : "r" (src)
        );

	printf("%d\n", dst);
}

输出部分#

表示当这段代码执行完后,对输出变量的约束。必要时在输出部分可以有多个约束,互相以逗号分隔。每个输出约束以=/+开头,跟着一个限定符表示操作数的类型(见下表)。

输入部分#

当输出约束为空,若有输入约束,则必须保留分隔标记.

会被修改的部分#

顾名思义

操作数#

分类 限定符 描述
通用寄存器 “a” 将输入变量放入eax这里有一个问题:假设eax已经被使用,那怎么办?其实很简单:因为GCC 知道eax 已经被使用,它在这段汇编代码的起始处插入一条语句pushl %eax,将eax 内容保存到堆栈,然 后在这段代码结束处再增加一条语句popl %eax,恢复eax的内容
“b” 将输入变量放入ebx
“c” 将输入变量放入ecx
“d” 将输入变量放入edx
“s” 将输入变量放入esi
“d” 将输入变量放入edi
“q” 将输入变量放入eax,ebx,ecx,edx中的一个
“r” 将输入变量放入通用寄存器,也就是eax,ebx,ecx,edx,esi,edi中的一个
“A” 把eax和edx合成一个64 位的寄存器(use long longs)
内存 “m” 内存变量
“o” 操作数为内存变量,但是其寻址方式是偏移量类型,也即是基址寻址,或者是基址加变址寻址
“V” 操作数为内存变量,但寻址方式不是偏移量类型
“ ” 操作数为内存变量,但寻址方式为自动增量
“p” 操作数是一个合法的内存地址(指针)
寄存器或内存 “g” 将输入变量放入eax,ebx,ecx,edx中的一个,或者作为内存变量
“X” 操作数可以是任何类型
立即数 “I” 0-31之间的立即数(用于32位移位指令)
“J” 0-63之间的立即数(用于64位移位指令)
“N” 0-255之间的立即数(用于out指令)
“i” 立即数
“n” 立即数,有些系统不支持除字以外的立即数, 这些系统应该使用“n”而不是“i”
匹配 “0”,“1”…“9” 表示用它限制的操作数与某个指定的操作数匹配,也即该操作数就是指定的那个操作数,例如“0”去描述“%1”操作数,那么“%1”引用的其实就是“%0”操作数,注意作为限定符字母的0-9与指令中的“%0”-“%9”的区别,前者描述操作数,后者代表操作数。
& 该输出操作数不能使用过和输入操作数相同的寄存器
操作数类型 “=” 操作数在指令中是只写的(输出操作数)
“+” 操作数在指令中是读写类型的(输入输出操作数)
浮点数 “f” 浮点寄存器
“t” 第一个浮点寄存器
“u” 第二个浮点寄存器
“G” 标准的80387浮点常数
% 该操作数可以和下一个操作数交换位置例如addl的两个操作数可以交换顺序(当然两个操作数都不能是立即数)
# 部分注释,从该字符到其后的逗号之间所有字母被忽略
* 表示如果选用寄存器,则其后的字母被忽略c9ff95d8-f7d1-40e8-91a6-80c98643d2ea

操作数的编号从输出部分的第一个约束开始顺序记数, 在汇编语句中引用这些操作数时用 %n 表示(n从0开始)

__asm__ __volatile__("movl %1,%0" : "=r" (result) : "m" (input));

参数调用#

int main()
{
    int a=0,b=0,c=2,d=3,e=4,f=5,g=6,h=7,i=8,j=9,k=10,m=11;
    asm(
        "add %2,%0\n\t"
        "add %13,%1\n\t"
        :"+r"(a),"+r"(b),"+r"(c)
        :"r"(d),"r"(e),"r"(f),"r"(g),"r"(h),"r"(i),"r"(j),"r"(k),"r"(m)
    );//此处给出的输出参数三个都是可读可写的"+r"而不是只可写的"=r",当调用参数的数字,超过输入参数数量的就用下几个(超过数量)可读的参数
//如此处都为"+r",则%12就是a,%13就是b,%14就是c
    printf("a=%d,b=%d,c=%d,d=%d",a,b,c,d);
    return 0;
}
int main()
{
    int a=0,b=0,c=2,d=3,e=4,f=5,g=6,h=7,i=8,j=9,k=10,m=11;
    asm(
        "add %2,%0\n\t"
        "add %13,%1\n\t"
        :"=r"(a),"+r"(b),"+r"(c)
        :"r"(d),"r"(e),"r"(f),"r"(g),"r"(h),"r"(i),"r"(j),"r"(k),"r"(m)
    );//此处a为只写所以不可读,%12为b,%13为c,%14就超出调用范围了
    printf("a=%d,b=%d,c=%d,d=%d",a,b,c,d);
    return 0;
}

/*
a=5, b=3, c=2, d=3

改 Intel 风格#

asm(
    ".intel_syntax noprefix\n"
    "...\n"
);
/*
编译选项加 -masm=intel

花指令#

jnx + jx#

asm volatile(
	".intel_syntax noprefix\n"
    "jz 1f\n"
    "jnz 1f\n"
    ".byte 0xE8\n"
    "1:\n\t"
);

永真条件跳转#

asm volatile(
    ".intel_syntax noprefix\n\t"
    "push rbx\n\t"          // 64 位必须 rbx
    "xor ebx, ebx\n\t"      // 32 位子寄存器清零并零扩展 rbx
    "test ebx, ebx\n\t"
    "jnz 1f\n\t"
    "jz 2f\n\t"
    "1:\n\t"
    ".byte 0xE8\n\t"
    "2:\n\t"
    "pop rbx\n\t"
    ".att_syntax prefix\n\t"
    :
    :
    : "rbx", "cc", "memory"
);
 ; __unwind {
 F3 0F 1E FA                               endbr64
 55                                        push    rbp
 48 89 E5                                  mov     rbp, rsp
 48 83 EC 10                               sub     rsp, 10h
 64 48 8B 04 25 28 00 00 00                         mov     rax, fs:28h
 48 89 45 F8                               mov     [rbp+var_8], rax
 31 C0                                     xor     eax, eax
 C6 45 F7 C3                               mov     [rbp+var_9], 0C3h
 48 8D 45 F7                               lea     rax, [rbp+var_9]
 FF D0                                     call    rax
 C6 45 08 96                               mov     byte ptr [rbp+8], 96h
 C6 45 09 46                               mov     byte ptr [rbp+9], 46h ; 'F'
 C6 45 0A 40                               mov     byte ptr [rbp+0Ah], 40h ; '@'
 90                                        nop
 48 8B 45 F8                               mov     rax, [rbp+var_8]
 64 48 33 04 25 28 00 00 00                xor     rax, fs:28h
 74 05                                     jz      short locret_425EF0
 E8 90 B1 FD FF                            call    ___stack_chk_fail
 ; ---------------------------------------------------------------------------

locret_425EF0:                          ; CODE XREF: y0u_d0nt_n33d_to_r3verse_th1s(void)+3Fj
 C9              						  leave
 C3             						  retn
 ; } // starts at 425EAA

重叠字节#

image-20250828110334075

asm volatile (
        ".intel_syntax noprefix\n"
        
        // 伪造指令序列:
        // 1. mov ax, 05EBh
        // 2. xor eax, eax
        // 3. jz -2
        
        // 实际执行序列:
        // 1. mov ax, 03EBh
        // 2. jmp short +5
        // 3. (跳转到真正的指令)
        
        // 构造字节序列:66 B8 EB 03 31 C0 74 FA E8
        
        "    .byte 0x66\n"   // 66: 操作数大小前缀,表示 mov ax
        "    .byte 0xB8\n"   // B8: mov reg, imm 的操作码
        "    .byte 0xEB\n"   // EB: mov ax 的立即数低位,同时是 jmp 的操作码
        "    .byte 0x05\n"   // 03: mov ax 的立即数高位,同时是 jmp 的偏移量
        
        "    .byte 0x31\n"   // 31: xor eax, eax 的操作码
        "    .byte 0xC0\n"   // C0: xor eax, eax 的 ModR/M 字节
        "    .byte 0x74\n"   // 74: jz 的操作码
        "    .byte 0xFA\n"   // FA: jz 的偏移量
        "    .byte 0xE8\n"   // E8: junk code

        "    .att_syntax\n"
    );

层面一:反汇编器看到的(静态分析)

反汇编器会从第一个字节 66 开始,顺序解析指令:

  • 66 B8 EB 05:这是一条 MOV 指令。
    • 66:操作码前缀,表示操作数大小为 16 位。
    • B8MOV AX, imm16 的操作码。
    • EB 05:16 位立即数 05EBh
    • 指令:mov ax, 05EBh。这条指令将 0x05EB 移动到 ax 寄存器。
  • 31 C0:这是一条 XOR 指令。
    • 31XOR 的操作码。
    • C0eax, eax 的 ModR/M 字节。
    • 指令:xor eax, eax。这条指令将 eax 寄存器清零。
  • 74 FA:这是一条 JZ 指令。
    • 74JZ 的操作码。
    • FA:跳转偏移量 -6
    • 指令:jz -6。如果前一条指令结果为零,就向后跳转 6 个字节。
  • E8:这是一个 CALL 指令的操作码,但它在这里只被看作一个字节。

层面二:实际执行的(动态执行)

实际的程序执行流会是完全不同的。mov ax, 05EBh 的立即数部分被精心设计过,它包含了另一条指令:

  • EB 05
    • EBjmp short 的操作码。
    • 05:跳转偏移量 +5
    • 指令:jmp +5。当执行到这里时,程序会立即跳转到当前位置往后 5 个字节的地方。

因此,实际执行流是:

  1. mov ax, 05EBh 执行时,会跳过后面的 xor eax, eaxjz -/3 指令。
  2. 程序跳转到 E8 所在的位置。
  3. E8 字节被解释为 call 指令的操作码。它会调用 Real Code,然后 Real Code 执行完毕后返回,程序继续正常执行。