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

基本格式

1
2
3
4
5
6
asm volatile(
"汇编语句"
: 输出部分
: 输入部分
: 会被修改的部分(clobbered register)
);

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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开始)

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

参数调用

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 风格

1
2
3
4
5
6
asm(
".intel_syntax noprefix\n"
"...\n"
);
/*
编译选项加 -masm=intel

花指令

jnx + jx

1
2
3
4
5
6
7
asm volatile(
".intel_syntax noprefix\n"
"jz 1f\n"
"jnz 1f\n"
".byte 0xE8\n"
"1:\n\t"
);

永真条件跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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"
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 ; __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)+3F↑j
C9 leave
C3 retn
; } // starts at 425EAA

重叠字节

image-20250828110334075

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 执行完毕后返回,程序继续正常执行。