0%

goto and computed goto

在網路上看到computed goto,才知道這不在C/C++標準裡面,只是gcc/clang中的extension,難怪我以前沒聽過
goto就不說了,goto fail太有名了,C/C++都已經建議少用goto,就介紹變種的computed goto

Example

gcc稱computed goto為"label as values",使用label當作pointer,然後用goto jump到執行的地方

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
#include <stdio.h>

int main() {
void *labels[] = {&&label1, &&label2, &&label3, &&label4};
int i = 1;

goto *labels[i];

label1:
printf("This is label 1\n");
goto end;

label2:
printf("This is label 2\n");
goto end;

label3:
printf("This is label 3\n");
goto end;

label4:
printf("Invalid label\n");
goto end;

end:
printf("End of program\n");
return 0;
}

Without computed gogo

將前面的範例,用符合標準的方式寫一次,能用的工具通常就是switch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

int main() {
int i = 1;

switch (i) {
case 0:
printf("This is label 1\n");
break;
case 1:
printf("This is label 2\n");
break;
case 2:
printf("This is label 3\n");
break;
default:
printf("Invalid label\n");
break;
}

printf("End of program\n");
return 0;
}

Generated assembly code

先看Switch的版本

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
62
63
64
65
66
67
68
69
70
main: # @main
push rbp
mov rbp, rsp
sub rsp, 48
mov dword ptr [rbp - 4], 0
mov dword ptr [rbp - 8], 1
mov eax, dword ptr [rbp - 8]
test eax, eax
mov dword ptr [rbp - 12], eax # 4-byte Spill
je .LBB0_1
jmp .LBB0_6
.LBB0_6:
mov eax, dword ptr [rbp - 12] # 4-byte Reload
sub eax, 1
mov dword ptr [rbp - 16], eax # 4-byte Spill
je .LBB0_2
jmp .LBB0_7
.LBB0_7:
mov eax, dword ptr [rbp - 12] # 4-byte Reload
sub eax, 2
mov dword ptr [rbp - 20], eax # 4-byte Spill
je .LBB0_3
jmp .LBB0_4
.LBB0_1:
movabs rdi, offset .L.str
mov al, 0
call printf
mov dword ptr [rbp - 24], eax # 4-byte Spill
jmp .LBB0_5
.LBB0_2:
movabs rdi, offset .L.str.1
mov al, 0
call printf
mov dword ptr [rbp - 28], eax # 4-byte Spill
jmp .LBB0_5
.LBB0_3:
movabs rdi, offset .L.str.2
mov al, 0
call printf
mov dword ptr [rbp - 32], eax # 4-byte Spill
jmp .LBB0_5
.LBB0_4:
movabs rdi, offset .L.str.3
mov al, 0
call printf
mov dword ptr [rbp - 36], eax # 4-byte Spill
.LBB0_5:
movabs rdi, offset .L.str.4
mov al, 0
call printf
xor ecx, ecx
mov dword ptr [rbp - 40], eax # 4-byte Spill
mov eax, ecx
add rsp, 48
pop rbp
ret
.L.str:
.asciz "This is label 1\n"

.L.str.1:
.asciz "This is label 2\n"

.L.str.2:
.asciz "This is label 3\n"

.L.str.3:
.asciz "Invalid label\n"

.L.str.4:
.asciz "End of program\n"

LBB0_1.LBB0_4分別對應四個switch case,main的前半段和LBB0_6LBB0_7來決定跳到哪個label去
如果用computed goto的話

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
62
63
64
65
66
67
68
69
70
71
72
73
74
main: # @main
push rbp
mov rbp, rsp
sub rsp, 96
mov dword ptr [rbp - 4], 0
mov rax, qword ptr [.L__const.main.labels]
mov qword ptr [rbp - 48], rax
mov rax, qword ptr [.L__const.main.labels+8]
mov qword ptr [rbp - 40], rax
mov rax, qword ptr [.L__const.main.labels+16]
mov qword ptr [rbp - 32], rax
mov rax, qword ptr [.L__const.main.labels+24]
mov qword ptr [rbp - 24], rax
mov dword ptr [rbp - 52], 1
movsxd rax, dword ptr [rbp - 52]
mov rax, qword ptr [rbp + 8*rax - 48]
mov qword ptr [rbp - 64], rax # 8-byte Spill
jmp .LBB0_6
.Ltmp1: # Block address taken
movabs rdi, offset .L.str
mov al, 0
call printf
mov dword ptr [rbp - 68], eax # 4-byte Spill
jmp .LBB0_5
.Ltmp2: # Block address taken
movabs rdi, offset .L.str.1
mov al, 0
call printf
mov dword ptr [rbp - 72], eax # 4-byte Spill
jmp .LBB0_5
.Ltmp3: # Block address taken
movabs rdi, offset .L.str.2
mov al, 0
call printf
mov dword ptr [rbp - 76], eax # 4-byte Spill
jmp .LBB0_5
.Ltmp4: # Block address taken
movabs rdi, offset .L.str.3
mov al, 0
call printf
mov dword ptr [rbp - 80], eax # 4-byte Spill
.LBB0_5:
movabs rdi, offset .L.str.4
mov al, 0
call printf
xor ecx, ecx
mov dword ptr [rbp - 84], eax # 4-byte Spill
mov eax, ecx
add rsp, 96
pop rbp
ret
.LBB0_6:
mov rax, qword ptr [rbp - 64] # 8-byte Reload
jmp rax
.L__const.main.labels:
.quad .Ltmp1
.quad .Ltmp2
.quad .Ltmp3
.quad .Ltmp4

.L.str:
.asciz "This is label 1\n"

.L.str.1:
.asciz "This is label 2\n"

.L.str.2:
.asciz "This is label 3\n"

.L.str.3:
.asciz "Invalid label\n"

.L.str.4:
.asciz "End of program\n"

這裡的Ltmp1Ltmp4一樣是對應四個case,而main的前半段就計算該跳到哪個label,在

1
2
3
.LBB0_6:
mov rax, qword ptr [rbp - 64] # 8-byte Reload
jmp rax

直接跳走了,根據benchmark的結果,這樣的作法比swithc快上不少,所以很多emulator和interpreter都採用這種作法

Computed goto and C++

有個很陰險的地方

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
#include <iostream>

class Test {
public:
Test() { std::cout << "Constructor called\n"; }
~Test() { std::cout << "Destructor called\n"; }
};

int main() {
void* labels[] = {&&label1, &&label2};
int i = 1;

{
Test t;
goto *labels[i];
}

label1:
std::cout << "This is label 1\n";
goto end;

label2:
std::cout << "This is label 2\n";
goto end;

end:
std::cout << "End of program\n";
return 0;
}

在這種情況下,Test的destructor不起作用
如果改成

1
2
3
4
{
Test t;
goto label2;
}

逕行了,不過這通常不是我們要的,可能要依需求修改程式