System Learning Daily 7

GCC 内联汇编的使用方法的自用记录。

GCC Extended Asm

asm-qualifiers

有三个可选:

volatile,inline,goto.

volatile

  1. 代码有副作用(side effect)的时候使用。
  2. 如果汇编语句中没有Output内容,则默认 volatile。
  3. 如果没有volatile且汇编语句的Ouput内容没有被该语句下面的代码使用,则该汇编语句有可能被编译器优化掉。

inline

简单来说就是使得生成的汇编代码体积最小。

goto

没用过,似乎也没有用的理由,跳过。

OutputOperand

给例子,直接对比。

推荐借助Compiler Explorer来学习以下内容。

example 1, “inc”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

void inc1() {
int a = 0;
__asm__ volatile ("inc %0" : "=r"(a) : "0"(a));
}
void inc2() {
int a = 0;
__asm__ volatile ("inc %0" : "=r"(a));
}
void inc3() {
int a = 0;
__asm__ volatile ("inc %0" : : "r"(a));
}
void inc4() {
int a = 0;
__asm__ volatile ("inc %0" : "+r"(a));
}

gcc 生成的无优化代码:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
inc1:
push ebp
mov ebp, esp
sub esp, 16
mov DWORD PTR [ebp-4], 0
mov eax, DWORD PTR [ebp-4]
inc eax
mov DWORD PTR [ebp-4], eax
nop
leave
ret
inc2:
push ebp
mov ebp, esp
sub esp, 16
mov DWORD PTR [ebp-4], 0
inc eax
mov DWORD PTR [ebp-4], eax
nop
leave
ret
inc3:
push ebp
mov ebp, esp
sub esp, 16
mov DWORD PTR [ebp-4], 0
mov eax, DWORD PTR [ebp-4]
inc eax
nop
leave
ret

inc4:
push ebp
mov ebp, esp
sub esp, 16
mov DWORD PTR [ebp-4], 0
mov eax, DWORD PTR [ebp-4]
inc eax
mov DWORD PTR [ebp-4], eax
nop
leave
ret
  1. inc1() 是正确写法。
  2. inc2() 没有提前将 a写入eax
  3. inc3() 没有在eax自增之后回写到a
  4. inc4() 和 inc1() 有着相同的机器码,也是正确写法。不知道是不是完全相同的写法。

example 2, “bts”

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
void bts1() {
int a = 1;
int b = 2;
__asm__ volatile ("bts %0, %1" : "+r"(a) : "r"(b) : "cc");
}

void bts2() {
int a = 1;
int b = 2;
__asm__ volatile ("bts %0, %1" : "=r"(a) : "r"(b) : "cc");
}

void bts3() {
int a = 1;
__asm__ volatile ("bts %0, %1" : "=r"(a) : "0"(a) : "cc");
}

void bts4() {
int a = 1;
__asm__ volatile ("bts %0, %1" : "+r"(a) : "r"(a) : "cc");
}

void bts5() {
int a = 1;
int b = 2;
__asm__ volatile ("bts %0, %1" : "=r"(a) : "0"(b) : "cc");
}

gcc 生成的无优化代码:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
bts1:
push ebp
mov ebp, esp
sub esp, 16
mov DWORD PTR [ebp-4], 1
mov DWORD PTR [ebp-8], 2
mov edx, DWORD PTR [ebp-8]
mov eax, DWORD PTR [ebp-4]
bts eax, edx
mov DWORD PTR [ebp-4], eax
nop
leave
ret
bts2:
push ebp
mov ebp, esp
sub esp, 16
mov DWORD PTR [ebp-4], 1
mov DWORD PTR [ebp-8], 2
mov eax, DWORD PTR [ebp-8]
bts eax, eax
mov DWORD PTR [ebp-4], eax
nop
leave
ret
bts3:
push ebp
mov ebp, esp
sub esp, 16
mov DWORD PTR [ebp-4], 1
mov eax, DWORD PTR [ebp-4]
bts eax, eax
mov DWORD PTR [ebp-4], eax
nop
leave
ret
bts4:
push ebp
mov ebp, esp
sub esp, 16
mov DWORD PTR [ebp-4], 1
mov edx, DWORD PTR [ebp-4]
mov eax, DWORD PTR [ebp-4]
bts eax, edx
mov DWORD PTR [ebp-4], eax
nop
leave
ret

bts5:
push ebp
mov ebp, esp
sub esp, 16
mov DWORD PTR [ebp-4], 1
mov DWORD PTR [ebp-8], 2
mov eax, DWORD PTR [ebp-8]
bts eax, eax
mov DWORD PTR [ebp-4], eax
nop
leave
ret
  1. bts1() 是bts a b的正确写法。
  2. bts2() 的OutputOperands前缀是=,则输入a与输出b都采用了同一个寄存器eax
  3. bts3() 在InputOperands中使用Operands标号可以将一个InputOperand和一个OutputOperand“连接(tie)”起来,使用同一个寄存器或者同一块内存地址。
  4. bts4() 是bts a a的正确写法,可以吧InputOperands直接全部去掉。
  5. bts5() 一个”连接(tie)“的例子,使用eax,输入b把结果放入a中。

example 3, multiple assembler instructions - 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// multiple assembler instruction
void mai() {
int a = 0;
int b = 1;
int c = 2;

// a = a + b + c, using ebx as strach register
__asm__ volatile (
"mov ebx, 0\n\t"
"add ebx, %[a]\n\t"
"add ebx, %[b]\n\t"
"add ebx, %[c]\n\t"
"mov %[a], ebx"
: [a] "+r"(a)
: [b] "r"(b), [c] "r"(c)
: "ebx", "cc"
);
}

gcc生成的无优化代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mai:
push ebp
mov ebp, esp
push ebx
sub esp, 16
mov DWORD PTR [ebp-8], 0
mov DWORD PTR [ebp-12], 1
mov DWORD PTR [ebp-16], 2
mov edx, DWORD PTR [ebp-12]
mov ecx, DWORD PTR [ebp-16]
mov eax, DWORD PTR [ebp-8]
mov ebx, 0
add ebx, eax
add ebx, edx
add ebx, ecx
mov eax, ebx
mov DWORD PTR [ebp-8], eax
nop
mov ebx, DWORD PTR [ebp-4]
leave
ret

这个例子中,最下面的ebx告诉编译器,我们需要使用这个寄存器,所以编译器将该寄存器直接压栈保存,在leave中弹栈返回。

example 4, multiple assembler instructions - 2

an example about earlyclobber

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int baz1() {
int a = 1;
asm volatile(
"add %1, %0\n\t"
"add %1, %0"
: "+r" (a) : "r" (1));
return a;
}
int baz2() {
int a = 1;
asm volatile(
"add %1, %0\n\t"
"add %1, %0"
: "+&r" (a) : "r" (1));
return a;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
; gcc -m32 -O2 ...`

baz1:
mov eax, 1
add eax, eax
add eax, eax
ret
baz2:
mov edx, 1
mov eax, edx
add edx, eax
add edx, eax
ret

对于 baz1()来说,编译器认为对 1的所有读都在写a之前,那么1a自然可以使用同一个寄存器eax来存储数据,但这是不正确的,因为并不是读都在写之前。

对于 baz2()来说,我们使用了&来告知编译器,所以编译器不会把一个寄存器既当做输入,又当做输出了。