逆向工程基本原理02

逆向工程基本原理02

exdoubled Lv4

分支循环的机器级表示

分支指令

直接跳转

1
B <target_address>

主要用于函数内跳转

间接跳转

1
BR Xm

跳转到寄存器Xm中存储的地址

带链接跳转的分支指令

函数调用

1
BL <target_address>

将当前指令地址+4(next pc)存储到链接寄存器LR中,然后跳转到目标地址,一般为CPU自己执行,不需要程序员关心

函数返回

1
RET (Xm)

若未指定 Xm ,则将 LR 寄存器中的地址作为返回地址跳转;若指定了 Xm ,则将 Xm 寄存器中的地址作为返回地址跳转

带条件的分支指令

B/BL <cond> <target>

根据 cmp 的结果来决定是否跳转到对应 label

1
2
CMP		X0, #5
B.GT label1 ;如果 X0 > 5 则跳转到 label1

CBNZ/CBZ

根据给定寄存器是否为0来来判断是否跳转

1
2
CBNZ	X0, label1			;如果 X0 != 0 则跳转到 label1
CBZ W0, label2 ;如果 W0 == 0 则跳转到 label2

TBZ/TBNZ

根据给定寄存器的某一位是否为0来判断是否跳转

1
2
TBZ		X0, #3, label1			;如果 X0 的第3位 == 0 则跳转到 label1
TBNZ X0, #20, label2 ;如果 X0 的第20位 != 0 则跳转到 label2

所有的条件后缀:

条件后缀含义(整数)含义(浮点数)
EQ等于等于
NE不等于不等于、无序
CS进位设置大于、等于、无序
CC进位清除小于
MI负数小于
PL正数、零大于、等于、无序
VS溢出无序
VC无溢出非无序
HI无符号大于大于、无序
LS无符号小于等于小于、等于
GE有符号大于等于大于、等于
LT有符号小于小于、无序
GT有符号大于大于
LE有符号小于等于小于、等于、无序
AL总是总是

带指令集切换的分支指令

BX/BLX <cond> <target>

带指令集跳转的切换,在 A32 和 T32 之间切换,根据目标地址的最后一位判断指令集

  • 如果 bit[0] == 0 则切换到 A32
  • 如果 bit[0] == 1 则切换到 T32

如果同时也是带链接的跳转,在发生跳转前会将当前指令地址+4(next pc)存储到链接寄存器 LR 中

if 语句的机器级表示

if 语句通常会被翻译为条件跳转语句

给出一个简单的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int max(int a, int b) {
if (a > b) {
return a;
} else {
return b;
}
}

int main() {
int x = 10;
int y = 20;
int result = max(x, y);
return result;
}

wsl ubantu24.04 arm64 编译汇编结果:

1
aarch64-linux-gnu-g++ -O0 -S if.cpp -o if.s
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
_Z3maxii:
.LFB0:
.cfi_startproc
sub sp, sp, #16
.cfi_def_cfa_offset 16
str w0, [sp, 12]
str w1, [sp, 8]
ldr w1, [sp, 12]
ldr w0, [sp, 8]
cmp w1, w0
ble .L2 # 注意这里是小于等于,和原条件判断的条件取反了
ldr w0, [sp, 12]
b .L3
.L2:
ldr w0, [sp, 8]
.L3:
add sp, sp, 16
.cfi_def_cfa_offset 0
ret
.cfi_endproc
.LFE0:
.size _Z3maxii, .-_Z3maxii
.align 2
.global main
.type main, %function

switch 语句的机器级表示

多个条件跳转1

当 case 较少时会这么做

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void process_command(int cmd, int& x) {
switch(cmd) {
case 1:
x = 1;
break;
case 100:
x = 2;
break;
case 1000:
x = 3;
break;
default:
x = 4;
}
}

int main() {
int x;
process_command(1, x); // 启动
process_command(100, x); // 停止
process_command(500, x); // 未知
}

汇编如下:

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
_Z15process_commandiRi:
.LFB0:
.cfi_startproc
sub sp, sp, #16
.cfi_def_cfa_offset 16
str w0, [sp, 12]
str x1, [sp]
ldr w0, [sp, 12]
cmp w0, 1000
beq .L2
ldr w0, [sp, 12]
cmp w0, 1000
bgt .L3
ldr w0, [sp, 12]
cmp w0, 1
beq .L4
ldr w0, [sp, 12]
cmp w0, 100
beq .L5
b .L3
.L4:
ldr x0, [sp]
mov w1, 1
str w1, [x0]
b .L6
.L5:
ldr x0, [sp]
mov w1, 2
str w1, [x0]
b .L6
.L2:
ldr x0, [sp]
mov w1, 3
str w1, [x0]
b .L6
.L3:
ldr x0, [sp]
mov w1, 4
str w1, [x0]
nop
.L6:
nop
add sp, sp, 16
.cfi_def_cfa_offset 0
ret
.cfi_endproc

跳转表

有时间补上,试了很多次都是条件分支/决策树,开 -O1 -O2 -O3 都一样,就算使用了 -fno-tree-switch-conversion 也没生成跳转表,有点诡异

多个条件跳转2

当switch case较多但case值不连续时,翻译为多个条件跳转

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
#include <iostream>
using namespace std;

int get_http_status(int code) {
switch(code) {
// 1xx 信息性状态码
case 100: return 1; // Continue
case 101: return 1; // Switching Protocols
case 102: return 1; // Processing

// 2xx 成功状态码
case 200: return 2; // OK
case 201: return 2; // Created
case 202: return 2; // Accepted

// 3xx 重定向状态码
case 300: return 3; // Multiple Choices
case 301: return 3; // Moved Permanently
case 302: return 3; // Found

// 4xx 客户端错误
case 400: return 4; // Bad Request
case 401: return 4; // Unauthorized
case 403: return 4; // Forbidden
case 404: return 4; // Not Found

// 5xx 服务器错误
case 500: return 5; // Internal Server Error
case 501: return 5; // Not Implemented
case 502: return 5; // Bad Gateway
case 503: return 5; // Service Unavailable

default: return -1;
}
}

int main() {
int codes[] = {100, 200, 300, 400, 404, 500, 600};

for(int code : codes) {
int category = get_http_status(code);
}

return 0;
}

汇编如下

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
_Z15get_http_statusi:
.LFB1986:
.cfi_startproc
sub sp, sp, #16
.cfi_def_cfa_offset 16
str w0, [sp, 12]
ldr w0, [sp, 12]
cmp w0, 503
beq .L2
ldr w0, [sp, 12]
cmp w0, 503
bgt .L3
ldr w0, [sp, 12]
cmp w0, 502
beq .L4
ldr w0, [sp, 12]
cmp w0, 502
bgt .L3
ldr w0, [sp, 12]
cmp w0, 501
beq .L5
ldr w0, [sp, 12]
cmp w0, 501
bgt .L3
ldr w0, [sp, 12]
cmp w0, 500
beq .L6
ldr w0, [sp, 12]
cmp w0, 500
bgt .L3
ldr w0, [sp, 12]
cmp w0, 404
beq .L7
ldr w0, [sp, 12]
cmp w0, 404
bgt .L3
ldr w0, [sp, 12]
cmp w0, 403
beq .L8
ldr w0, [sp, 12]
cmp w0, 403
bgt .L3
ldr w0, [sp, 12]
cmp w0, 401
beq .L9
ldr w0, [sp, 12]
cmp w0, 401
bgt .L3
ldr w0, [sp, 12]
cmp w0, 400
beq .L10
ldr w0, [sp, 12]
cmp w0, 400
bgt .L3
ldr w0, [sp, 12]
cmp w0, 302
beq .L11
ldr w0, [sp, 12]
cmp w0, 302
bgt .L3
ldr w0, [sp, 12]
cmp w0, 301
beq .L12
ldr w0, [sp, 12]
cmp w0, 301
bgt .L3
ldr w0, [sp, 12]
cmp w0, 300
beq .L13
ldr w0, [sp, 12]
cmp w0, 300
bgt .L3
ldr w0, [sp, 12]
cmp w0, 202
beq .L14
ldr w0, [sp, 12]
cmp w0, 202
bgt .L3
ldr w0, [sp, 12]
cmp w0, 201
beq .L15
ldr w0, [sp, 12]
cmp w0, 201
bgt .L3
ldr w0, [sp, 12]
cmp w0, 200
beq .L16
ldr w0, [sp, 12]
cmp w0, 200
bgt .L3
ldr w0, [sp, 12]
cmp w0, 102
beq .L17
ldr w0, [sp, 12]
cmp w0, 102
bgt .L3
ldr w0, [sp, 12]
cmp w0, 100
beq .L18
ldr w0, [sp, 12]
cmp w0, 101
beq .L19
b .L3
.L18:
mov w0, 1
b .L20
.L19:
mov w0, 1
b .L20
.L17:
mov w0, 1
b .L20
.L16:
mov w0, 2
b .L20
.L15:
mov w0, 2
b .L20
.L14:
mov w0, 2
b .L20
.L13:
mov w0, 3
b .L20
.L12:
mov w0, 3
b .L20
.L11:
mov w0, 3
b .L20
.L10:
mov w0, 4
b .L20
.L9:
mov w0, 4
b .L20
.L8:
mov w0, 4
b .L20
.L7:
mov w0, 4
b .L20
.L6:
mov w0, 5
b .L20
.L5:
mov w0, 5
b .L20
.L4:
mov w0, 5
b .L20
.L2:
mov w0, 5
b .L20
.L3:
mov w0, -1
.L20:
add sp, sp, 16
.cfi_def_cfa_offset 0
ret
.cfi_endproc

位图实现

这个我看没人讲过,但实际上是存在这种优化的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int get_month_days(int month) {
switch(month) {
case 1: case 3: case 5: case 7: case 8: case 10: case 12:
return 31;
case 4: case 6: case 9: case 11:
return 30;
case 2:
return 28;
default:
return -1;
}
}

int main() {
int m = 5;
int days = get_month_days(m);
}

汇编如下:

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
_Z14get_month_daysi:
.LFB0:
.cfi_startproc
sub sp, sp, #16
.cfi_def_cfa_offset 16
str w0, [sp, 12]
ldr w0, [sp, 12]
cmp w0, 12
cset w1, hi
and w1, w1, 255
and w1, w1, 1
cmp w1, 0
bne .L2
mov x1, 1
lsl x0, x1, x0
mov x1, 5546
and x1, x0, x1
cmp x1, 0
cset w1, ne
and w1, w1, 255
and w1, w1, 1
cmp w1, 0
bne .L3
mov x1, 2640
and x1, x0, x1
cmp x1, 0
cset w1, ne
and w1, w1, 255
and w1, w1, 1
cmp w1, 0
bne .L4
and x0, x0, 4
cmp x0, 0
cset w0, ne
and w0, w0, 255
and w0, w0, 1
cmp w0, 0
bne .L5
b .L2
.L3:
mov w0, 31
b .L6
.L4:
mov w0, 30
b .L6
.L5:
mov w0, 28
b .L6
.L2:
mov w0, -1
.L6:
add sp, sp, 16
.cfi_def_cfa_offset 0
ret
.cfi_endproc

这个没有开任何优化选项,位图在 -O0 就已经实现了,这个优化位于编译器前端

可以看到,编译器首先将输入的 month 转换为一个位图,1月对应 bit0,2月对应 bit1,…,12月对应 bit11,然后通过位运算判断输入的 month 是否在对应的 case 中,最后根据结果返回对应的天数

这种优化大大减少了条件跳转的数量,提升了性能

其他优化

  • 决策树,类似二分查找
  • 范围折叠,比如 case 1-10 执行一样的代码,编译器直接生成 x - 1 <= 9

来看一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int classify(int x) {
switch(x) {
case 1: case 2: case 3: case 4: return 10;
case 100: case 101: case 102: return 100;
case 500: case 501: case 502: return 200;
case 1000: case 1001: return 300;
case 5000: case 5001: case 5002: return 400;
default: return -1;
}
}

int main(){
int x = classify(10);
int y = classify(50);
}


开启了 -O2 优化

汇编如下:

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
_Z8classifyi:
.LFB0:
.cfi_startproc
mov w1, w0
cmp w0, 502
bgt .L2
mov w0, 200
cmp w1, 499
bgt .L1
cmp w1, 4
bgt .L4
cmp w1, 0
mov w0, 10
csinv w0, w0, wzr, gt
.L1:
ret
.p2align 2,,3
.L2:
cmp w0, 1001
bgt .L5
cmp w0, 1000
mov w0, 300
csinv w0, w0, wzr, ge
ret
.p2align 2,,3
.L4:
sub w1, w1, #100
mov w0, 100
cmp w1, 3
csinv w0, w0, wzr, cc
ret
.p2align 2,,3
.L5:
mov w0, -5000
add w1, w1, w0
cmp w1, 3
mov w0, 400
csinv w0, w0, wzr, cc
ret
.cfi_endproc
.LFE0:
.size _Z8classifyi, .-_Z8classifyi
.section .text.startup,"ax",@progbits
.align 2
.p2align 4,,11
.global main
.type main, %function

while 语句的机器级表示

while 循环存在模板如下:

1
2
3
4
5
6
7
	do sth...
b L2
L1:
do loop
L2:
do cond_check
b.cond L1
1
2
3
4
5
6
7
8
9
int while_loop(){
int sum = 0;
int i = 1;
while(i < 100){
sum += i;
i++;
}
return sum;
}

汇编如下:

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
_Z10while_loopv:
.LFB0:
.cfi_startproc
sub sp, sp, #16
.cfi_def_cfa_offset 16
str wzr, [sp, 8]
mov w0, 1
str w0, [sp, 12]
b .L2
.L3:
ldr w1, [sp, 8]
ldr w0, [sp, 12]
add w0, w1, w0
str w0, [sp, 8]
ldr w0, [sp, 12]
add w0, w0, 1
str w0, [sp, 12]
.L2:
ldr w0, [sp, 12]
cmp w0, 99
ble .L3
ldr w0, [sp, 8]
add sp, sp, 16
.cfi_def_cfa_offset 0
ret
.cfi_endproc

可以看到,.L2是 while 循环的条件检查部分,.L3 是循环体部分,循环体执行完后无条件跳转回条件检查部分

for 语句的机器级表示

for 和 while 在大多数情况下可以相互转换

1
2
3
4
5
6
7
8
int for_loop(){
int sum = 0;
int i = 1;
for(int i = 1; i < 100; ++i){
sum += i;
}
return sum;
}

汇编如下:

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
_Z8for_loopv:
.LFB1:
.cfi_startproc
sub sp, sp, #16
.cfi_def_cfa_offset 16
str wzr, [sp, 4]
mov w0, 1
str w0, [sp, 12]
mov w0, 1
str w0, [sp, 8]
b .L6
.L7:
ldr w1, [sp, 4]
ldr w0, [sp, 8]
add w0, w1, w0
str w0, [sp, 4]
ldr w0, [sp, 8]
add w0, w0, 1
str w0, [sp, 8]
.L6:
ldr w0, [sp, 8]
cmp w0, 99
ble .L7
ldr w0, [sp, 4]
add sp, sp, 16
.cfi_def_cfa_offset 0
ret
.cfi_endproc

可以看到,.L6 是 for 循环的条件检查部分,.L7 是循环体部分,循环体执行完后无条件跳转回条件检查部分,而 for 循环的 i++ 操作被放在了循环体的末尾

do-while 语句的机器级表示

1
2
3
4
5
6
7
8
int dowhile_loop(){
int sum = 0;
int i = 1;
do{
sum += i;
} while(i++ < 100);
return sum;
}

汇编如下

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
_Z12dowhile_loopv:
.LFB2:
.cfi_startproc
sub sp, sp, #16
.cfi_def_cfa_offset 16
str wzr, [sp, 8]
mov w0, 1
str w0, [sp, 12]
.L10:
ldr w1, [sp, 8]
ldr w0, [sp, 12]
add w0, w1, w0
str w0, [sp, 8]
ldr w0, [sp, 12]
add w1, w0, 1
str w1, [sp, 12]
cmp w0, 99
cset w0, le
and w0, w0, 255
and w0, w0, 1
cmp w0, 0
bne .L10
ldr w0, [sp, 8]
add sp, sp, 16
.cfi_def_cfa_offset 0
ret
.cfi_endproc

可以看到,.L10 是 do-while 循环的循环体部分,循环体执行完后会进行条件检查,如果条件满足则跳转回循环体部分继续执行,否则退出循环

函数调用的机器级表示

寄存器使用规范

  • 参数寄存器 X0-X7 用于传递函数参数,X0 储存函数返回值
  • X8 是 间接结果寄存器,通常用于储存间接结果的地址(比如返回一个很大的结构体)
  • 调用者保存寄存器 X9-X15 用于保存函数调用过程中需要保留的值
  • X16 和 X17 是函数内暂存寄存器(IP0,IP1),例程和它调用的任何子例程之间的暂存寄存器
  • X18 是平台寄存器 PR,通常用于指向线程局部存储(TLS)或其他平台特定数据结构
  • 被调用者保存寄存器 X19-X28 是保存应在调用之间保留的长期使用的寄存器
  • X29 是帧指针寄存器 FP,在打开tail call优化时为普通寄存器
  • X30 是链接寄存器 LR,保存函数调用结束时的返回地址
  • SP 是栈指针寄存器

浮点寄存器:

  • V0 储存函数返回值
  • V1-V7 是调用者保存寄存器
  • V8-V15 是被调用者保存寄存器
  • V16-V31 是临时寄存器

栈空间约定

  • 函数中的局部变量

  • 参数传递过程中参数较多X0-X7/V0-V7不足以存放所有寄存器时,将参数存放到栈上

  • 存放返回地址

  • 每个函数仅应该使用自己的栈空间,当前函数的栈空间范围由FP、SP规定

  • 栈空间的申请在被调用函数开头完成

  • 栈空间从高地址向低地址生长

函数调用规范与机器级表示

调用者函数

  • 保存caller-saved registers(根据具体情况,并非所有调用都需保存)
  • 按照参数传递约定设置参数
  • 将返回地址存放到LR寄存器中(通过branch-with-link跳转实现)

被调用者函数

  • 申请callee的栈空间
  • 将FP和LR保存到栈上(非叶子函数)
  • 将参数存放到栈上
  • 保存callee-saved registers(根据具体情况,并非所有调用都需保存)
  • 执行callee函数逻辑

函数返回规范与机器级表示

调用者函数

  • 恢复callee-saved registers
  • 将函数返回值保存到X0/V0
  • 恢复FP和LR(非叶子函数)
  • 释放申请的栈空间
  • 根据LR返回caller

被调用者函数

  • 保存X0/V0中的函数返回值
  • 恢复caller-saved registers

C++成员函数调用

对象成员函数

需要将当前对象的地址放到第一个参数寄存器,即 this->func(xxx) 调用语句实际实现为 func(this, xxx),其中 this 是一个指向当前对象的指针,存放在 X0 寄存器中

类成员函数

和普通函数调用一样,参数按照约定传递即可

普通函数和虚函数调用的区别:

静态绑定与动态绑定:

在继承场景下,父类指针调用普通函数和虚函数时会产生差异,普通函数调用时会根据指针类型进行静态绑定,调用父类的函数实现;而虚函数调用时会根据对象实际类型进行动态绑定,调用子类的函数实现

  • 静态绑定:若父类指针调用的是非虚函数,在编译的时候直接调用这个指针声明类型的类函数,即使实际对象是子类
  • 动态绑定:当调用的是虚函数时,在运行时判断指针指向的对象,并根据虚函数表调用相应的函数

C++ 函数重载

C++支持函数重载,即相同的函数名可以根据函数参数的不同来决定调用哪个函数,C++ 中引入 Name Mangling(名称修饰)机制来实现函数重载的支持,编译器会根据函数的参数类型、数量等信息对函数名进行修饰,使得每个重载的函数在编译后的符号表中具有唯一的名称,从而实现函数重载的功能

GCC/Clang 的 Name Mangling 规则

遵循 Itanium C++ ABI 规范(所有字符均为 ASCII 可打印字符,以 _Z 开头)

基础规则:

1
_Z <qualified-name> <type-info>

基本上为:

1
函数名 命名空间/类 参数类型 模板参数 CV限定符

限定名编码

普通函数

1
void fn(int)

编码为:

1
2
3
4
→  _Z2fni
│ │ └─ 参数 int → i
│ └── 名字长度2 + "fn"
└──── 前缀 _Z

命名空间/类嵌套

使用 N...E 包裹

嵌套名用 N 开头、E 结束,每段由 长度+名字 拼接:

1
namespace foo { class Bar { void fn(); }; }

编码为:

1
2
3
4
5
6
7
_ZN3foo3Bar2fnEv
│ │ │ │ │└── 无参数 → v
│ │ │ │ └─── E 结束嵌套
│ │ │ └────── 2fn(名字)
│ │ └────────── 3Bar(类名)
│ └────────────── 3foo(命名空间)
└──────────────── N 开始嵌套

类型编码

C++ 类型编码C++ 类型编码
voidvboolb
intiunsigned intj
longlunsigned longm
long longxunsigned long longy
floatfdoubled
charcunsigned charh
signed charawchar_tw
...(varargs)z

指针与引用在类型前加前缀:

1
2
3
4
*        → P
& → R
const → K
&& → O

重复类型压缩:

1
2
3
4
第1次重复 → S_
第2次重复 → S0_
第3次重复 → S1_
...

函数重载

函数重载时,编译器会根据函数参数的类型、数量等信息对函数名进行修饰,使得每个重载的函数在编译后的符号表中具有唯一的名称,从而实现函数重载的功能

1
2
3
4
void fn(int)            → _Z2fni
void fn(double) → _Z2fnd
void fn(int, double) → _Z2fnid
void fn(double, int) → _Z2fndi

CV 限定符

成员函数的 constvolatile 限定符追加在参数列表之后

1
2
3
4
K  = const
V = volatile
KV = const volatile
r = restrict(C99 扩展)
1
2
3
4
5
struct Foo {
void fn(); → _ZN3Foo2fnEv
void fn() const; → _ZN3Foo2fnEKv
void fn() volatile; → _ZN3Foo2fnEVv
};

模板

模板函数不编码返回类型

函数模板

模板参数放在 I...E 块中

1
2
3
4
template<typename T>
void fn(T x);

fn<int>(42);

编码为

1
2
3
4
5
6
_Z2fnIiEvi
│ │││└─ 参数 int → i(实际参数)
│ ││└── E 结束模板参数
│ │└─── 模板参数 int → i
│ └───── I 开始模板参数
└────────── 函数名 fn

类模板

1
2
3
4
template<typename T>
struct Vec { void push(T); };

Vec<int>::push(int) → _ZN3VecIiE4pushEi

C++ 不允许仅凭返回类型重载,所以对于普通函数,返回类型对于区分符号没有任何作用,Itanium ABI 直接省略它以缩短符号长度。模板函数则不同——不同的模板参数可以推导出不同的返回类型,编译器需要把它编进符号里才能正确区分特化版本

1
2
非模板:_ZN3foo3Bar2fnEi       → 无返回类型段
模板: _ZN3foo3Bar2fnIiEEvi → v(返回 void)在 E 之后立即出现

特殊成员函数

构造函数、析构函数和运算符有专用编码:

特殊函数编码示例
构造函数(完整对象)C1_ZN3FooC1Ev
构造函数(基类子对象)C2_ZN3FooC2Ev
析构函数(完整对象)D1_ZN3FooD1Ev
析构函数(基类子对象)D2_ZN3FooD2Ev
析构函数(删除对象)D0_ZN3FooD0Ev
operator+pl_ZplRKFooS0_
operator<<ls
operator[]ix
operator newnw_ZnwmPv
operator deletedl
类型转换运算符cv <type>_ZN3FoocvdEvoperator double

extern "C" 与跨语言链接

1
2
extern "C" void fn(int);   // → fn(不加任何修饰,C 语言链接)
void fn(int); // → _Z2fni(C++ 链接)

extern "C" 块内的函数完全绕过 mangling,使用 C 链接规则,这是 C/C++ 互操作的基础机制。

std 命名空间压缩

std 命名空间有一套固定缩写:

展开压缩
std::St
std::stringSs
std::istreamSi
std::ostreamSo
std::iostreamSd
std::allocator<T>Sa

lambda 表达式

Lambda 被编译器命名为 <lambda N>,N 是出现顺序:

1
2
auto lam = [](int x){ return x; };
// → _ZZ4mainENKUliE_clEi(在 main 内的第0个 lambda 的 operator())

Ul...E_ 是 lambda 的编码块:U = unnamed type,l = lambda,参数列表,E_ 结束。

虚表、RTTI 相关符号

实体前缀示例
vtable_ZTV_ZTV3Foo
VTT(虚表表)_ZTT_ZTT3Foo
typeinfo_ZTI_ZTI3Foo
typeinfo name_ZTS_ZTS3Foo
thunk_ZTh虚继承调整

例子:

可以使用 c++filt 工具来解码 mangled name:

先举一个编码例子:

1
_ZNK3foo3Bar2fnIiEERKdSs

解码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_Z          全局前缀
N 嵌套名开始
K const 成员函数
3foo 命名空间 foo
3Bar 类 Bar
2fn 方法名 fn
I 模板参数开始
i T = int
E 模板参数结束
E 嵌套名结束
RKd 返回类型 const double& ← 仅因为是模板函数才出现
Ss 参数 std::string(固定压缩)

→ const double& foo::Bar::fn<int>(std::string) const
1
2
# c++filt _ZNK3foo3Bar2fnIiEERKdSs
double const& foo::Bar::fn<int>(std::basic_string<char, std::char_traits<char>, std::allocator<char> >) const

由于

1
typedef std::basic_string<char, std::char_traits<char>, std::allocator<char>> string;

所以其实是对的

编译器在 mangle 之前就已经把所有 typedef 展开成底层类型了,符号里根本不存在 string 这个名字,Itanium ABI 把这个常用的完整模板实例列为特殊缩写

1
2
3
4
源码:       std::string
编译器: std::basic_string<char, std::char_traits<char>, std::allocator<char>>
mangled: Ss(这是对上面完整类型的压缩,不是对 string 这个名字的压缩)
c++filt 后: std::basic_string<char, std::char_traits<char>, std::allocator<char>>

MSVC 的 Name Mangling 规则

MSVC 使用完全独立的一套规则,设计哲学与 Itanium 截然不同:名字反向排列、调用约定内嵌、类型用单字母大写编码,所有符号以 ? 开头,@ 作为各段分隔符

基础规则

1
? <name> @ <scope...> @ <calling-conv> <return-type> <params> @Z

限定名编码

顺序和 Itanium 相反,名字在前,限定符在后,使用 @ 分隔

1
2
3
4
void foo::Bar::fn(int)

Itanium: _ZN3foo3Bar2fnEi (外→内:foo → Bar → fn)
MSVC: ?fn@Bar@foo@@QAEHH@Z (内→外:fn → Bar → foo)

即:

1
2
3
4
5
6
7
8
9
10
11
?fn@Bar@foo@@QAEHH@Z

? 开头标志
fn 函数名
@Bar 所属类
@foo 所属命名空间
@@ 作用域结束(双 @)
QAE 调用约定 + 访问属性(public __thiscall)
H 返回类型 int
H 参数 int
@Z 结束

类型编码

MSVC 用大写字母,与 Itanium 的小写体系完全不一样

C++ 类型MSVC 编码Itanium 编码
voidXv
intHi
unsigned intIj
longJl
unsigned longKm
long long_Jx
floatMf
doubleNd
bool_Nb
charDc
unsigned charEh
wchar_t_Ww

指针与引用前缀

1
2
3
4
*        → P
& → A
const → B
volatile → C

例:

1
2
3
4
int*        →  PAH    (P = pointer, A = no CV, H = int
const int* → PBH (B = const pointer target)
int& → AAH (A = reference)
const int& → ABH

类型:

1
2
3
class → ?A V name @@
struct → ?A U name @@
enum → ?A W4 name @@

函数重载

和 Itanium 一样,参数类型序列决定最终符号,但用的是 MSVC 的类型字母

1
2
3
void fn(int)        →  ?fn@@YAXH@Z
void fn(double) → ?fn@@YAXN@Z
void fn(int, double)→ ?fn@@YAXHN@Z

CV 限定符

MSVC 将 const/volatile 编码在调用约定字段内,而不是独立前缀

1
2
3
4
5
Q  = public
A = non-const, non-volatile
B = const
C = volatile
D = const volatile

例子:

1
2
3
4
5
6
7
8
9
10
void Foo::fn()               →  QAE(public, non-const
void Foo::fn() const → QBE(public, const
void Foo::fn() volatile → QCE(public, volatile
void Foo::fn() const volatile→ QDE

struct Foo {
void fn(); // ?fn@Foo@@QAEXXZ
void fn() const; // ?fn@Foo@@QBEXXZ ← Q[B]E
void fn() volatile; // ?fn@Foo@@QCEXXZ ← Q[C]E
};

模板

MSVC 的模板参数用 $ 和尖括号风格的特殊序列编码,比 Itanium 的 I...E 更复杂:

1
2
3
4
template<typename T>
void fn(T x);

fn<int>(42);

编码如下:

1
2
3
4
??$fn@H@@YAXH@Z
│└── $fn@H = fn<int>(H=int)
└─── ??$ 开启模板实例

类模板成员函数

1
2
3
4
5
template<typename T>
struct Vec { void push(T); };

Vec<int>::push(int) → ?push@?$Vec@H@@QAEXH@Z
^^── ?$Vec@H = Vec<int>

非类型模板函数

1
2
3
4
5
template<int N>
void fn();

fn<42>() → ??$fn@$0CK@@@YAXXZ
^^^── $0CK = 整数 42(MSVC 用 $0 前缀 + 特殊编码)

特殊成员函数

特殊函数MSVC 编码示例
构造函数?0??0Foo@@QAE@XZ
析构函数?1??1Foo@@QAE@XZ
operator new?2??2@YAPAXI@Z
operator delete?3??3@YAXPAX@Z
operator=?4??4Foo@@QAEAAV0@ABV0@@Z
operator==?8
operator[]?A
operator()?R
operator<<?6
类型转换 operator double?Bdouble 前缀

构造函数和析构函数不单独写名字,而用 ?0 / ?1 这类数字编号代替:

1
2
3
4
5
??0Foo@@QAE@XZ
│ │ │└─ 无参数 X,结尾 Z
│ │ └── @ 分隔
│ └─────────── 0 = 构造函数,Foo 为类名
└────────────── ?? 前缀(特殊成员)

extern "C" 与跨语言链接

行为和 GCC 完全一致

名称压缩:反向引用表

MSVC 也有类似 Itanium 的压缩机制,但方式不同——维护一张最多 10 个槽位(0~9)的反向引用表,已出现的类型/名称用 0~9 引用

1
2
void fn(Foo, Foo);  →  ?fn@@YAXVFoo@@0@Z
// └── 0 = 第一个 Foo 的反向引用

超过 10 个不重复类型就不再压缩,直接写全名

这比 Itanium 的 S_/S0_ 机制容量小

虚表、RTTI 相关符号

实体MSVC 符号前缀示例
vtable??_7??_7Foo@@6B@
typeinfo??_R0??_R0?AVFoo@@@8
RTTI 完整对象定位器??_R4
动态 atexit 析构??__F

调用约定

MSVC 与 Itanium 差异最大的地方之一,调用约定被直接编进符号里,不同约定产生不同符号

访问权限 + 调用约定编码说明
public __cdeclYA普通全局函数默认
public __stdcallYGWin32 API 常用
public __fastcallYI寄存器传参
public __thiscallQAE成员函数默认(32位)
public __thiscall(64位)QEAx64 成员函数
static __cdecl 成员SA静态成员
private __thiscallAAE
protected __thiscallIAE

32位和64位的编码也不同,因为 x64 只有一种调用约定(__fastcall 变体),MSVC 用 E 后缀区分

其实是 Windows 的⑩山的一种表现形式罢了

例子:

https://www.demangler.com/

这个网站是一个在线的 C++ name demangler,可以输入 mangled name 来查看它的解码结果,支持 Itanium 和 MSVC 两种 mangling 规则

举个例子

1
?fn@Bar@foo@@QBE?ANH@Z

分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
?           开头
fn 函数名
@Bar 类 Bar
@foo 命名空间 foo
@@ 作用域结束
Q public 成员
B const 修饰(B = const)
E __thiscall 调用约定(32位)
?AN 返回类型 double(?A = 修饰符无,N = double)
H 参数 int
@Z 结束

→ public: double __thiscall foo::Bar::fn(int) const

用刚刚那个网站验证一下:

1
double foo::Bar::fn(int)const 

一些碎碎念

编译器和工具的文档往往少得可耻

其实可以看出,Itanium 的 ABI 相对来说更可读,也更适合流式解析,设计干净,毕竟原来设计的初衷就是跨平台一致;而 Microsoft ABI 是没有官方文档的,虽然有社区逆向吧,但没有官方文档始终是一件很麻烦的事,并且模板代码生成的符号极长。Windows 为了兼容性做出了太多的妥协,这个我认为的设计比较混乱 Name Mangling 的规则其实只是 Windows ABI 复杂性的一个缩影

调用约定其实体现了两套 ABI 设计哲学最不同的一面:

Itanium 认为调用约定是平台/编译器的事,为了跨平台,不编进符号,同一个函数无论用什么调用约定,mangled name 不变

Microsoft 认为调用约定是函数类型的一部分,必须编进符号,主要还是Win32 历史上 __cdecl__stdcall__fastcall 并存,同一个名字不同调用约定是不同的函数,链接器必须能区分,但是,x64 时代只有一种调用约定,为了兼容,这部分的历史包袱没办法丢掉,于是现在的 mangled name 又臭又长

下面给出 Itanium ABI 官方文档链接:

Itanium C++ ABI

LLVM 为了兼容 Microsoft ABI 整理过一份实现

llvm-project/clang/lib/AST/MicrosoftMangle.cpp at main · llvm/llvm-project

还有一份丹麦技术大学的研究员 Agner Fog 整理的一分文档,值得收藏:

Calling conventions

浮点指令集

VFP(Vector Floating Point)向量浮点运算单元

  • 支持常见的浮点计算,如加法、减法、乘法、除法、比较
  • 支持向量 (Vector) 计算功能
  • 在不支持 VFP 的处理器上,编译器会使用浮点支持软件库fplib实现CPU指令模拟浮点计算

SIMD 指令集

NEON 是 ARM 的 SIMD(Single Instruction, Multiple Data)指令集扩展,提供了对向量化计算的支持,适用于多媒体处理、信号处理、机器学习等领域

  • NEON计算库:如Arm Compute Library、Arm Performance Libraries等
  • 编译器支持:通过编译器的自动向量化,将C/C++代码自动编译为SIMD指令
  • 内嵌函数:编译器会将NEON内嵌函数转换成使用NEON指令的汇编代码,并自动进行寄存器分配等处理
  • 汇编代码:直接编写使用SIMD指令集的汇编代码

编译器优化会单独开一篇来讲

同分类文章

Comments