X64 汇编程序
64 位与 32 位的区别
更大的内存
- 64 位 CPU 与 32 位 CPU 的区别
- 引脚根数:
- x86 程序:20 根
- x64 程序:52 根,实际寻址的有 48 根,所以最大内存是 0~256T
- 寻址区间:
- x86 程序:0x00000000 ~ 0xFFFFFFFF
- x64程序:0xXXXX0000`00000000 ~ 0xXXXXFFFF`FFFFFFFF ,intel 高16 位必须是第 48 位符号扩展,所以实际如下:
- R3 的逻辑地址范围:0x00000000`00000000 ~ 0xFFFFFFFF`FFFFFFFF
- R0 的逻辑地址范围:0xFFFF8000`00000000 ~ 0xFFFFFFFF`FFFFFFFF
- 引脚根数:
- 64 位 CPU 的三种运行模式
- 实模式:在此模式下,可以跑16 位程序,微软的操作系统禁用此模式。
- 64 位模式:只能运行 64 位的程序。
- 兼容模式:能运行 x32 位和 x64 位程序。模拟执行 32 位程序。
更快的运行速度
x64 程序最先是 AMD 提出来的,随后 inter 跟着推出了 IA(安腾处理器)但是由于其不兼容 x86 指令集,所以并没有得到太大的适用性。随后 AMD64 的问世,由于指令集并没有变动太多,且还能兼容运行 x86 的程序,所以得到了大量的普适性,Intel 也推出了 AMD64 标准的指令集 IA32e 。
寄存器的扩展:
- 从原来的 32 位寄存器扩展到了 64 位寄存器。
- 通用寄存器的数量得到了大量的提升(可以寄存器传参,局部变量寄存器存储)。
x64 汇编基础知识
寄存器
-
通用寄存器
eax
、ebx
、ecx
、edx
、ebp
、esi
、edi
、esp
全部又原先的 32 位扩展至 64 位,前缀由e
改为r
。- 增加了
r8 ~ r9
8 个 64 位通用寄存器。
-
多媒体相关寄存器:
- x87 浮点寄存器(MMX 寄存器)无变化
- SSE(AVX)寄存器数量由原理的 8 个增加至 16 个(
YMM/XMM8 ~ YMM/XMM15
)
-
标志寄存器:
- 扩展为 64 位,但高 32 位保留不使用(填充 0)。
-
段寄存器:
- 64 位模式下忽略了
ds
、es
、ss
寄存器。
- 64 位模式下忽略了
-
eip
寄存器扩展为rip
寄存器。64 位模式下只能使用rip
寄存器。
x64 寄存器后缀访问表示访问寄存器大小:b
(低8位)、w
(低16位)、d
(低32位),例: r8b
、r9w
、r10d
、r11
。
注意: 操作数为低 8 位、16 位,传送不影响高位;操作数为低 32 位,高位被扩展(零扩展,填充 0);
模式
64 位操作系统在只有 64 位模式下可以使用扩展寄存器,兼容模式可以运行 32 位和 64 位程序,但不能使用扩展寄存器。具体兼容模式和 64 为模式能够使用的寄存器种类如下图所示:
指令集
32 位原有指令集就能够满足常规使用。
x64 调用约定
64 位汇编与 32 位汇编的最大区别就是函数调用方式的变化。x64 体系结构利用机会清除了现有 Win32 调用约定(如 __stdcall
、__cdecl
、__fastcall
、_thiscall
等)的混乱,只保留了 4 寄存器 fastcall
一种调用约定。
- 整数传参:
- 前 4 个参数依序传入
RCX
、RDX
、R8
、R9
。 - 前 4 个以外的整数参数将使用栈传参。
- 前 4 个参数依序传入
- 浮点参数:
- 4 个参数依序传入
XMM0
、XMM1
、XMM2
、XMM3
,后续的浮点参数将放置到线程堆栈上。
- 4 个参数依序传入
- 混合传参:
- 如果整型和浮点型参数混合会放到对应的寄存器中。例如
fun(int, float, int, float)
函数对应参数会依次放到ecx
,xmm1
,r8d
,xmm3
寄存器中。
- 如果整型和浮点型参数混合会放到对应的寄存器中。例如
- 返回值:整型放
rax
,浮点型放xmm0
。
x64 汇编程序设计
开发环境
- 64 位汇编需要使用 VS 的 x64 编译器
ml64.exe
将.asm
文件编译为.obj
文件。该编译器的路径为Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\bin\Hostx64\x64\ml64.exe
。 - 选用 VS 的配套链接器
link.exe
,可以链接x64位的程序,link.exe
与ml64.exe
在同一目录下。 - 由于编译链接需要很多外部库,为了避免配置环境变量等出错,这里直接在 VS 自带的
x64 Native Tools Command Prompt for VS
中进行编译链接。编译链接命令如下:ml64 /c Hello.asm link /subsystem:windows /entry:Entry Hello.obj
第一个 x64 汇编程序
includelib kernel32.lib
includelib user32.lib
extern MessageBoxA:proc
extern ExitProcess:proc
extern GetCurrentProcessId:proc
extern wsprintfA:proc
.const
MSG1 db "Hello x64! pid:%d %d %d %d", 0
TITLE1 db "sky123", 0
.data
BUFFER db 260 dup(0)
.code
Entry proc
push rbx
push rcx
sub rsp, 38h ;保证栈地址模16 必须申请32个字节栈预留空间
call GetCurrentProcessId
mov rcx, offset BUFFER
mov rdx, offset MSG1
mov r8d, eax
mov r9d, 5
mov dword ptr [rsp + 20h], 6
mov dword ptr [rsp + 28h], 7
call wsprintfA
mov rcx, 0
mov rdx, offset BUFFER
mov r8, offset TITLE1
mov r9d, 0
call MessageBoxA
mov ecx, 0
call ExitProcess
sub rsp, 38h
pop rcx
pop rbx
ret
Entry endp
end
使用运行时库
如果想使用运行时库函数 sprintf_s
来代替 wsprintfA
需要注意程序入口点应当指定为 WinMainCRTStartup
并且程序从 main
函数开始。因为运行时库函数使用前需要事先初始化相关变量。
编译链接脚本如下:
ml64 /c Hello.asm
link /entry:WinMainCRTStartup /subsystem:windows Hello.obj libcmt.lib msvcrt.lib
示例代码如下:
includelib kernel32.lib
includelib user32.lib
extern MessageBoxA:proc
extern ExitProcess:proc
extern GetCurrentProcessId:proc
extern sprintf_s:proc
.const
MSG1 db "Hello x64! pid:%d %d %d", 0
TITLE1 db "sky123", 0
.data
BUFFER db 260 dup(0)
.code
WinMain proc
push rbx
push rcx
sub rsp, 38h ;保证栈地址模16 必须申请32个字节栈预留空间
call GetCurrentProcessId
mov rcx, offset BUFFER
mov rdx, sizeof BUFFER
mov r8, offset MSG1
mov r9d, eax
mov dword ptr [rsp + 20h], 6
mov dword ptr [rsp + 28h], 7
call sprintf_s
mov rcx, 0
mov rdx, offset BUFFER
mov r8, offset TITLE1
mov r9d, 0
call MessageBoxA
mov ecx, 0
call ExitProcess
sub rsp, 38h
pop rcx
pop rbx
ret
WinMain endp
end
使用资源
以对话框为例,x64 汇编程序使用资源的示例代码如下:
includelib kernel32.lib
includelib user32.lib
extern MessageBoxA:proc
extern DialogBoxParamA:proc
extern EndDialog:proc
extern ExitProcess:proc
IDD_DIALOG1 equ 101
IDC_BUTTON1 equ 1001
IDC_BUTTON2 equ 1002
WM_CLOSE equ 10h
WM_COMMAND equ 111h
.const
MSG1 db "WM_COMMAND", 0
TITLE1 db "sky123", 0
.data
BUFFER db 260 dup(0)
.code
DialogProc proc
mov [rsp + 8], rcx
mov [rsp + 10h], rdx
mov [rsp + 18h], r8
mov [rsp + 20h], r9
sub rsp, 28h
xor eax, eax
cmp edx, WM_CLOSE
jz HANDLE_CLOSE
cmp edx, WM_COMMAND
jz HANDLE_COMMAND
jmp HANDLE_DEFAULT
HANDLE_COMMAND:
xor ecx, ecx
mov rdx, offset MSG1
mov r8, offset TITLE1
xor r9d, r9d
call MessageBoxA
inc eax
jmp HANDLE_DEFAULT
HANDLE_CLOSE:
mov rcx, [rsp + 30h]
xor edx, edx
call EndDialog
inc eax
HANDLE_DEFAULT:
add rsp, 28h
ret
DialogProc endp
WinMain proc
sub rsp, 38h
mov rdx, IDD_DIALOG1
xor r8d, r8d
mov r9, offset DialogProc
mov qword ptr [rsp + 20h], 0
call DialogBoxParamA
xor ecx, ecx
call ExitProcess
sub rsp, 38h
ret
WinMain endp
end
链接时需要将资源也链接进去,资源可以通过 VS 工程创建并编译。
ml64 /c Hello.asm
link /entry:WinMainCRTStartup /subsystem:windows Hello.obj res.res msvcrt.lib
如何寻找 main 函数
从入口函数开始找,第一个 exit
的调用,离该函数最近且有三个参数的函数为 main
函数。
制作 IDA 签名文件
在 VS 的 项目属性 → 配置属性 → VC++目录 → 常规 → 库目录
中包含了 VC 编译时使用的 lib 库。我们可以将这些库制作成签名文件方便逆向分析。
这次制作签名文件我们使用高版本的 Flair
,高本版的 Flair
可以自动将多个 lib 库制作为一个签名文件,不需要手动写脚本实现自动化。
将需要制作签名的 lib 库拷贝到同一目录下,然后运行命令 pcf.exe *.lib vs2022.pat
提取特征。之后运行 sigmake.exe .\vs2022.pat .\vs2022.sig
制作签名文件。
正常情况下制作签名文件时会出现冲突的情况,并且会生成一个 vs2022.exc
文件记录具体冲突的函数。
> sigmake.exe .\vs2022.pat .\vs2022.sig
.\vs2022.sig: modules/leaves: 16108/35926, COLLISIONS: 1996
See the documentation to learn how to resolve collisions.
vs2022.exc
文件中的内容如下:
;--------- (delete these lines to allow sigmake to read this file)
; add '+' at the start of a line to select a module
; add '-' if you are not sure about the selection
; do nothing if you want to exclude all modules
??0CppInlineNamespaceAttribute@?A0x020fbda2@vc.cppcli.attributes@@$$FQE$AAM@PE$AAVString@System@@@Z 00 0000 0330010007000000000000000228........2A..........................
??0CppInlineNamespaceAttribute@?A0x0f11cde5@vc.cppcli.attributes@@$$FQE$AAM@PE$AAVString@System@@@Z 00 0000 0330010007000000000000000228........2A..........................
??0CppInlineNamespaceAttribute@?A0x9646ae10@vc.cppcli.attributes@@$$FQE$AAM@PE$AAVString@System@@@Z 00 0000 0330010007000000000000000228........2A..........................
...
我们有两种方式解决冲突:
- 删掉签名的 4 行注释,这样 IDA 会使用
unknown_libname
来替代。 - 删除冲突的函数中选择使用的函数名。
解决冲突后再次运行 sigmake.exe .\vs2022.pat .\vs2022.sig
命令就可以制作出签名文件。
将制作的签名文件放置在 IDA 安装路径下的 sig\pc
目录下就可以 Shift + F5
打开签名窗口然后 Insert
加载签名文件。
最终可以成功识别出 mian
函数的位置。
Code = invoke_main();
if ( !(unsigned __int8)j_unknown_libname_2() )
j_exit(Code);
if ( !v2 )
j__cexit();
表达式
加法、减法、乘法
在 x64 位汇编对应的加法、减法、乘法指令的速度相对于 32 位汇编有了很大的提升,因此多数情况都是直接使用对应的指令而不作优化。少部分可以情况可以适用移位和比例因子进行优化。
除法
- 除数为变量时无法优化,根据除数类型选择
div
或idiv
。 - 除数为常量时可以优化。
被除数无符号,除数为 2 的整数次幂
直接 shr
移位即可。
被除数有符号,除数为 2 的整数次幂
如果是正数直接 sar
移位即可。如果是负数由于算术移位是向下取整,需要改为向上取整,因此
[
−
a
2
n
]
=
⌊
−
a
+
2
n
−
1
2
n
⌋
=
⌈
−
a
2
n
⌉
\left [ \frac{-a}{2^n} \right ] =\left \lfloor \frac{-a+2^n-1}{2^n} \right \rfloor =\left \lceil \frac{-a}{2^n}\right \rceil
[2n−a]=⌊2n−a+2n−1⌋=⌈2n−a⌉ 。
例如 ⌊ x 8 ⌋ \left \lfloor \frac{x}{8} \right \rfloor ⌊8x⌋ 对应的汇编代码如下:
.text:0000000140001000 mov rax, rcx ; rcx = x
.text:0000000140001003 cqo ; rax 符号扩展至 rdx
.text:0000000140001005 and edx, 7 ; rdx = x < 0 ? 7 : 0
.text:0000000140001008 add rax, rdx
.text:000000014000100B sar rax, 3 ; rax = (x + (x < 0 ? 7 : 0)) >> 3
注意:如果除数为 2 的整数次幂且无符号则无论被除数正负均向下取整。
被除数无符号,除数为非 2 的整数次幂
对于被除数无符号除数为非 2 的整数次幂的情况,为了避免使用除法指令,可以做如下转换:
⌊
a
b
⌋
=
⌊
a
×
⌈
2
n
b
⌉
2
n
⌋
(
n
≥
⌈
log
2
a
⌉
)
\left \lfloor \frac{a}{b} \right \rfloor =\left \lfloor \frac{a\times \left \lceil \frac{2^n}{b} \right \rceil }{2^n} \right \rfloor\ (n\ge \left \lceil \log_2 a \right \rceil )
⌊ba⌋=⌊2na×⌈b2n⌉⌋ (n≥⌈log2a⌉)
例如
⌊
a
35
⌋
\left \lfloor \frac{a}{35} \right \rfloor
⌊35a⌋ 对应的汇编代码如下:
.text:0000000140001000 mov rax, 0EA0EA0EA0EA0EA0Fh
.text:000000014000100A mul rcx ; a
.text:000000014000100D shr rdx, 5
.text:0000000140001011 mov eax, edx
其中 0EA0EA0EA0EA0EA0Fh
即
⌈
2
69
35
⌉
\left \lceil\frac{2^{69}}{35} \right \rceil
⌈35269⌉ ,因此整段汇编计算的是
⌊
a
×
⌈
2
69
35
⌉
2
69
⌋
\left \lfloor \frac{a\times \left \lceil \frac{2^{69}}{35} \right \rceil }{2^{69}} \right \rfloor
⌊269a×⌈35269⌉⌋ 。
当然如果除数过小可能会出现 MagicNumber
超过
2
64
2^{64}
264 的情况,例如
⌊
a
7
⌋
\left \lfloor \frac{a}{7} \right \rfloor
⌊7a⌋ 对应的 MagicNumber
为
⌈
2
67
7
⌉
\left \lceil\frac{2^{67}}{7} \right \rceil
⌈7267⌉ 即 12492492492492493h
超过了
2
64
2^{64}
264 ,需要通过进行多次移位在不超过
2
64
2^{64}
264 的常量参与运算的前提下实现同等的效果。因此实际上
⌊
a
7
⌋
\left \lfloor \frac{a}{7} \right \rfloor
⌊7a⌋ 对应的汇编代码如下:
.text:0000000140001000 mov rax, 2492492492492493h
.text:000000014000100A mul rcx
.text:000000014000100D sub rcx, rdx
.text:0000000140001010 shr rcx, 1
.text:0000000140001013 lea rax, [rdx+rcx]
.text:0000000140001017 shr rax, 2
这段汇编代码实现了如下计算:
⌊
a
×
⌈
2
67
7
⌉
2
67
⌋
=
⌊
a
−
⌊
a
×
(
⌈
2
67
7
⌉
−
2
64
)
2
64
⌋
2
+
⌊
a
×
(
⌈
2
67
7
⌉
−
2
32
)
2
64
⌋
2
2
⌋
\left \lfloor \frac{a\times \left \lceil \frac{2^{67}}{7} \right \rceil }{2^{67}} \right \rfloor=\left \lfloor \frac{\frac{a-\left \lfloor \frac{a\times (\left \lceil \frac{2^{67}}{7} \right \rceil-2^{64} )}{2^{64}} \right \rfloor }{2}+\left \lfloor \frac{a\times (\left \lceil \frac{2^{67}}{7} \right \rceil-2^{32} )}{2^{64}} \right \rfloor }{2^2} \right \rfloor
267a×⌈7267⌉
=
222a−⌊264a×(⌈7267⌉−264)⌋+⌊264a×(⌈7267⌉−232)⌋
其中
⌈
2
67
7
⌉
−
2
64
<
2
64
\left \lceil \frac{2^{67}}{7} \right \rceil-2^{64}<2^{64}
⌈7267⌉−264<264 ,满足条件。
被除数有符号,除数为非 2 的整数次幂
对于被除数为有符号数的情况,汇编代码如下:
.text:0000000140001000 mov rax, 0EA0EA0EA0EA0EA1h
.text:000000014000100A imul rcx
.text:000000014000100D sar rdx, 1
.text:0000000140001010 mov rax, rdx
.text:0000000140001013 shr rax, 63 ; 取 (MagicNumber * a) >> 63 的符号位
.text:0000000140001017 add rax, rdx ; 如果结果为负数则需要将结果加上 1
64 位汇编的优化与 32 位汇编相同,在被除数为负数时会在结果上加 1 实现向上取整的效果。编译器会选择合适的
2
n
2^n
2n 来确保 MagicNumber * a
不会被
2
n
2^n
2n 整除。
如果输出为负数则将符号转移至 MagicNumber
。例如
⌊
a
−
7
⌋
\left \lfloor \frac{a}{-7} \right \rfloor
⌊−7a⌋ 汇编代码如下,其中 4924924924924925h
即
⌈
2
65
7
⌉
\left \lceil \frac{2^{65}}{7} \right \rceil
⌈7265⌉ 。
.text:0000000140001000 mov rax, -4924924924924925h
.text:000000014000100A imul rcx
.text:000000014000100D sar rdx, 1
.text:0000000140001010 mov rax, rdx
.text:0000000140001013 shr rax, 3Fh
.text:0000000140001017 add rax, rdx
取模
- 模(除)数为变量时无法优化,根据模(除)数类型选择
div
或idiv
。 - 模(除)数为常量时可以优化。
被除数无符号,模(除)数为 2 的整数次幂
如果被除数为无符号数,则 a m o d 2 n a\mod 2^n amod2n 相当于 a & ( 2 n − 1 ) a\&(2^n-1) a&(2n−1) 。如果模数比较小会使用 32 位寄存器减短指令长度。
被除数有符号,模(除)数为 2 的整数次幂
64 位汇编的取模优化策略与 32 位不同,这里采用了下面的公式进行优化:
a
m
o
d
2
n
=
{
a
&
(
2
n
−
1
)
(
a
≥
0
)
(
a
+
2
n
−
1
)
&
(
2
n
−
1
)
−
(
2
n
−
1
)
(
a
<
0
)
a \bmod 2^n = \left\{\begin{aligned} &a\&(2^n-1) \quad &&(a \geq 0)\\ &(a+2^n-1)\&(2^n-1)-(2^n-1) \quad &&(a < 0) \end{aligned}\right.
amod2n={a&(2n−1)(a+2n−1)&(2n−1)−(2n−1)(a≥0)(a<0)
证明如下:
-
对于 a ≥ 0 a\ge0 a≥0 的情况显然成立。
-
对于 a < 0 a<0 a<0 的情况,首先 ( a + 2 n − 1 ) & ( 2 n − 1 ) (a+2^n-1)\&(2^n-1) (a+2n−1)&(2n−1) 可以看做为:
( a + 2 n − 1 ) & ( 2 n − 1 ) = { 2 n − 1 ( a m o d 2 n = 0 ) ( ∣ a ∣ m o d 2 n ) − 1 ( a m o d 2 n ≠ 0 ) (a+2^n-1)\&(2^n-1)=\left\{\begin{aligned} &2^n-1 &&(a \bmod 2^n= 0)\\ &(\left | a \right | \bmod 2^n)-1 \quad &&(a \bmod 2^n \neq 0) \end{aligned}\right. (a+2n−1)&(2n−1)={2n−1(∣a∣mod2n)−1(amod2n=0)(amod2n=0)- 对于 a m o d 2 n = 0 a \bmod 2^n= 0 amod2n=0 的情况 ( a + 2 n − 1 ) & ( 2 n − 1 ) − ( 2 n − 1 ) = 0 (a+2^n-1)\&(2^n-1)-(2^n-1)=0 (a+2n−1)&(2n−1)−(2n−1)=0 符合题意。
- 对于 a m o d 2 n ≠ 0 a \bmod 2^n \neq 0 amod2n=0 的情况, ( a + 2 n − 1 ) & ( 2 n − 1 ) − ( 2 n − 1 ) = ( ∣ a ∣ m o d 2 n ) − 1 − ( 2 n − 1 ) = ( ∣ a ∣ m o d 2 n ) − 2 n = a m o d 2 n (a+2^n-1)\&(2^n-1)-(2^n-1)=(\left | a \right | \bmod 2^n)-1-(2^n-1)=(\left | a \right | \bmod 2^n)-2^n=a\bmod 2^n (a+2n−1)&(2n−1)−(2n−1)=(∣a∣mod2n)−1−(2n−1)=(∣a∣mod2n)−2n=amod2n ,同样符合题意。
综上原命题得证。
以 a m o d 8 a\bmod 8 amod8 为例,对应的汇编代码如下:
.text:0000000140001000 mov rax, rcx ; rcx = a
.text:0000000140001003 cqo ; rdx = a >= 0 ? 0 : -1
.text:0000000140001005 and edx, 7 ; rdx = a >= 0 ? 0 : 7
.text:0000000140001008 add rax, rdx
.text:000000014000100B and eax, 7
.text:000000014000100E sub rax, rdx
高版本的 clang 编译器采用了另一种优化方式:
a
m
o
d
2
n
=
{
a
−
(
a
&
∼
(
2
n
−
1
)
)
(
a
≥
0
)
a
−
(
(
a
+
2
n
−
1
)
&
∼
(
2
n
−
1
)
)
(
a
<
0
)
a \bmod 2^n=\left\{\begin{aligned} &a - (a\& \sim (2^n-1))\quad&&(a \ge 0)\\ &a - ((a+2^n-1)\& \sim (2^n-1)) \quad &&(a < 0) \end{aligned}\right.
amod2n={a−(a&∼(2n−1))a−((a+2n−1)&∼(2n−1))(a≥0)(a<0)
证明如下:
- 对于 a ≥ 0 a\ge0 a≥0 的情况显然成立。
- 对于
a
<
0
a<0
a<0 的情况:
- 当 a m o d 2 n = 0 a \bmod 2^n= 0 amod2n=0 时, ( a + 2 n − 1 ) & ∼ ( 2 n − 1 ) = a & ∼ ( 2 n − 1 ) = a (a+2^n-1)\& \sim (2^n-1)=a\& \sim (2^n-1)=a (a+2n−1)&∼(2n−1)=a&∼(2n−1)=a ,所以 a − ( ( a + 2 n − 1 ) & ∼ ( 2 n − 1 ) ) = 0 a - ((a+2^n-1)\& \sim (2^n-1)) =0 a−((a+2n−1)&∼(2n−1))=0 显然成立。
- 当 a m o d 2 n ≠ 0 a \bmod 2^n\not = 0 amod2n=0 时, ( a + 2 n − 1 ) & ∼ ( 2 n − 1 ) = ( a & ∼ ( 2 n − 1 ) ) + 2 n (a+2^n-1)\& \sim (2^n-1)=(a\& \sim (2^n-1))+2^n (a+2n−1)&∼(2n−1)=(a&∼(2n−1))+2n,因此 a − ( ( a + 2 n − 1 ) & ∼ ( 2 n − 1 ) ) = ( a − ( a & ∼ ( 2 n − 1 ) ) ) − 2 n = ( ∣ a ∣ m o d 2 n ) − 2 n = a m o d 2 n a - ((a+2^n-1)\& \sim (2^n-1))=(a-(a\& \sim (2^n-1)))-2^n=(\left | a \right | \bmod 2^n)-2^n=a\bmod 2^n a−((a+2n−1)&∼(2n−1))=(a−(a&∼(2n−1)))−2n=(∣a∣mod2n)−2n=amod2n ,同样成立。
综上,原命题得证。
以 a m o d 8 a\bmod 8 amod8 为例,对应的汇编代码如下:
.text:0000000000401140 mov rax, [rsp] ; rax = a
.text:0000000000401144 lea rcx, [rax+7] ; rcx = a + 7
.text:0000000000401148 test rax, rax
.text:000000000040114B cmovns rcx, rax ; rcx = a >= 0 ? a : a + 7
.text:000000000040114F and rcx, 0FFFFFFFFFFFFFFF8h ; rcx = rcx & ~((1 << 3) - 1)
.text:0000000000401153 sub rax, rcx ; rax = a - rcx
被除数无符号,模数不为 2 的整数次幂
对于被除数无符号,模数不为 2 的整数次幂的情况, a m o d b a \bmod b amodb 会被编译器优化为 a − ⌊ a b ⌋ × b a-\left \lfloor \frac{a}{b} \right \rfloor \times b a−⌊ba⌋×b,其中 ⌊ a b ⌋ \left \lfloor \frac{a}{b} \right \rfloor ⌊ba⌋ 会按照被除数无符号,除数为非 2 的整数次幂的除法优化。
以 x % 7
为例,汇编代码如下:
.text:0000000140001000 mov rax, 2492492492492493h
.text:000000014000100A mul rcx ; rcx = x
.text:000000014000100D mov rax, rcx
.text:0000000140001010 sub rax, rdx
.text:0000000140001013 shr rax, 1
.text:0000000140001016 add rax, rdx
.text:0000000140001019 shr rax, 2
.text:000000014000101D imul eax, 7 ; eax = x / 7 * 7
.text:0000000140001020 sub ecx, eax ; ecx = x - x / 7 * 7
.text:0000000140001022 mov eax, ecx
被除数有符号,模数不为 2 的整数次幂
如果模数为正数,则 a m o d b a \bmod b amodb 会同样按照 a − ⌊ a b ⌋ × b a-\left \lfloor \frac{a}{b} \right \rfloor \times b a−⌊ba⌋×b 来进行优化,只不过这里的除法按照被除数有符号,除数为非 2 的整数次幂的情况进行优化。
.text:0000000140001000 mov rax, 4924924924924925h
.text:000000014000100A imul rcx
.text:000000014000100D sar rdx, 1
.text:0000000140001010 mov rax, rdx
.text:0000000140001013 shr rax, 3Fh
.text:0000000140001017 add rdx, rax
.text:000000014000101A imul eax, edx, 7 ; eax = x / 7 * 7
.text:000000014000101D sub ecx, eax ; ecx = x - x / 7 * 7
.text:000000014000101F mov eax, ecx
如果模数为负数,结果与模数为正数相同,即 a m o d b = a m o d ∣ b ∣ a \bmod b = a \bmod \left | b \right | amodb=amod∣b∣ 。
控制流程
32 位程序和 64 位程序的控制流程基本不变,这里只介绍有变化的部分。
三目运算符
相较于 32 位汇编,64 位汇编普便采用 cmovxx
指令,而不会针对每种类型的三目运算进行无分支优化。
- 如果三目运算符中的表达式不是特别复杂会采用
cmovxx
指令。 - 如果三目运算符中的表达式比较复杂会转换为
if-else
形式。 - 特别的,如果是
x == a ? b : b + 1
形式的三目运算会使用setnz
进行优化。
switch 语句
分支较多但比较连续
为了节省空间 64 位的跳转表并没有像 32 位那样存放分支的地址,而是存放分支地址相对于某一基址的偏移,而这一基址即 PE 文件的 ImageBase
(PE 文件的实际加载基址,PE 文件加载时会通过重定位表进行修正) ,跳转表中元素的大小则为 4 字节。
.text:0000000140001000 sub rsp, 28h
.text:0000000140001004 dec ecx ; switch 6 cases
.text:0000000140001006 cmp ecx, 5
.text:0000000140001009 ja def_140001023 ; jumptable 0000000140001023 default case
.text:000000014000100F movsxd rax, ecx
.text:0000000140001012 lea rdx, cs:140000000h ; ImageBase
.text:0000000140001019 mov ecx, ds:(jpt_140001023 - 140000000h)[rdx+rax*4] ; mov ecx, [rdx+rax*4+10B4h]
.text:0000000140001020 add rcx, rdx ; 跳转表中的偏移还要加上 ImageBase 才是分支的地址
.text:0000000140001023 jmp rcx ; switch jump
.text:0000000140001025 $LN4:
.text:0000000140001025 lea rcx, string1 ; jumptable 0000000140001023 case 1
.text:000000014000102C call cs:__imp_puts
.text:0000000140001032 xor eax, eax
.text:0000000140001034 add rsp, 28h
.text:0000000140001038 retn
.text:0000000140001039 $LN5:
.text:0000000140001039 lea rcx, string2 ; jumptable 0000000140001023 case 2
.text:0000000140001040 call cs:__imp_puts
.text:0000000140001046 xor eax, eax
.text:0000000140001048 add rsp, 28h
.text:000000014000104C retn
.text:000000014000104D $LN6:
.text:000000014000104D lea rcx, string3 ; jumptable 0000000140001023 case 3
.text:0000000140001054 call cs:__imp_puts
.text:000000014000105A xor eax, eax
.text:000000014000105C add rsp, 28h
.text:0000000140001060 retn
.text:0000000140001061 $LN7:
.text:0000000140001061 lea rcx, string4 ; jumptable 0000000140001023 case 4
.text:0000000140001068 call cs:__imp_puts
.text:000000014000106E xor eax, eax
.text:0000000140001070 add rsp, 28h
.text:0000000140001074 retn
.text:0000000140001075 $LN8:
.text:0000000140001075 lea rcx, string5 ; jumptable 0000000140001023 case 5
.text:000000014000107C call cs:__imp_puts
.text:0000000140001082 xor eax, eax
.text:0000000140001084 add rsp, 28h
.text:0000000140001088 retn
.text:0000000140001089 $LN9:
.text:0000000140001089 lea rcx, string6 ; jumptable 0000000140001023 case 6
.text:0000000140001090 call cs:__imp_puts
.text:0000000140001096 xor eax, eax
.text:0000000140001098 add rsp, 28h
.text:000000014000109C retn
.text:000000014000109D def_140001023:
.text:000000014000109D lea rcx, string7 ; jumptable 0000000140001023 default case
.text:00000001400010A4 call cs:__imp_puts
.text:00000001400010AA xor eax, eax
.text:00000001400010AC add rsp, 28h
.text:00000001400010B0 retn
.text:00000001400010B4 jpt_140001023 dd offset $LN4 - 140000000h
.text:00000001400010B4 dd offset $LN5 - 140000000h ; jump table for switch statement
.text:00000001400010B4 dd offset $LN6 - 140000000h
.text:00000001400010B4 dd offset $LN7 - 140000000h
.text:00000001400010B4 dd offset $LN8 - 140000000h
.text:00000001400010B4 dd offset $LN9 - 140000000h
.text:00000001400010B4 main endp
分支较多且比较不连续
.text:0000000140001000 sub rsp, 28h
.text:0000000140001004 dec ecx ; switch 123 cases
.text:0000000140001006 cmp ecx, 7Ah
.text:0000000140001009 ja def_14000102B ; jumptable 000000014000102B default case, cases 6-122
.text:000000014000100F movsxd rax, ecx
.text:0000000140001012 lea rdx, cs:140000000h
.text:0000000140001019 movzx eax, ds:(byte_1400010D8 - 140000000h)[rdx+rax]
.text:0000000140001021 mov ecx, ds:(jpt_14000102B - 140000000h)[rdx+rax*4]
.text:0000000140001028 add rcx, rdx
.text:000000014000102B jmp rcx ; switch jump
.text:000000014000102D $LN4:
.text:000000014000102D lea rcx, string1 ; jumptable 000000014000102B case 1
.text:0000000140001034 call cs:__imp_puts
.text:000000014000103A xor eax, eax
.text:000000014000103C add rsp, 28h
.text:0000000140001040 retn
.text:0000000140001041 $LN5:
.text:0000000140001041 lea rcx, string2 ; jumptable 000000014000102B case 2
.text:0000000140001048 call cs:__imp_puts
.text:000000014000104E xor eax, eax
.text:0000000140001050 add rsp, 28h
.text:0000000140001054 retn
.text:0000000140001055 $LN6:
.text:0000000140001055 lea rcx, string3 ; jumptable 000000014000102B case 3
.text:000000014000105C call cs:__imp_puts
.text:0000000140001062 xor eax, eax
.text:0000000140001064 add rsp, 28h
.text:0000000140001068 retn
.text:0000000140001069 $LN7:
.text:0000000140001069 lea rcx, string4 ; jumptable 000000014000102B case 4
.text:0000000140001070 call cs:__imp_puts
.text:0000000140001076 xor eax, eax
.text:0000000140001078 add rsp, 28h
.text:000000014000107C retn
.text:000000014000107D $LN8:
.text:000000014000107D lea rcx, string5 ; jumptable 000000014000102B case 5
.text:0000000140001084 call cs:__imp_puts
.text:000000014000108A xor eax, eax
.text:000000014000108C add rsp, 28h
.text:0000000140001090 retn
.text:0000000140001091 $LN9:
.text:0000000140001091 lea rcx, string6 ; jumptable 000000014000102B case 123
.text:0000000140001098 call cs:__imp_puts
.text:000000014000109E xor eax, eax
.text:00000001400010A0 add rsp, 28h
.text:00000001400010A4 retn
.text:00000001400010A5 def_14000102B:
.text:00000001400010A5 lea rcx, string7 ; jumptable 000000014000102B default case, cases 6-122
.text:00000001400010AC call cs:__imp_puts
.text:00000001400010B2 xor eax, eax
.text:00000001400010B4 add rsp, 28h
.text:00000001400010B8 retn
.text:00000001400010BC jpt_14000102B dd offset $LN4 - 140000000h
.text:00000001400010BC dd offset $LN5 - 140000000h ; jump table for switch statement
.text:00000001400010BC dd offset $LN6 - 140000000h
.text:00000001400010BC dd offset $LN7 - 140000000h
.text:00000001400010BC dd offset $LN8 - 140000000h
.text:00000001400010BC dd offset $LN9 - 140000000h
.text:00000001400010BC dd offset def_14000102B - 140000000h
.text:00000001400010D8 byte_1400010D8 db 0, 1, 2, 3
.text:00000001400010D8 db 4, 6, 6, 6 ; indirect table for switch statement
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 6, 6
.text:00000001400010D8 db 6, 6, 5
函数
x64 调用约定
默认情况下,x64 应用程序二进制接口 (ABI) 使用四寄存器 fast-call
调用约定。 系统在调用堆栈上分配空间作为影子存储,供被调用方保存这些寄存器。
函数调用的参数与用于这些参数的寄存器之间有着严格的一一对应关系。 任何无法放入 8 字节或者不是 1、2、4 或 8 字节的参数都必须按引用传递。 单个参数永远不会分布在多个寄存器中。
整数参数在寄存器 RCX
、RDX
、R8
和 R9
中传递。 浮点数参数在 XMM0L
、XMM1L
、XMM2L
和 XMM3L
中传递。 16 字节参数按引用传递。
参数传递
前 4 个整数参数从左到右分别在 RCX
、RDX
、R8
和 R9
中传递。 如前所述,第 5 个和更高位置的参数在堆栈上传递。 寄存器中的所有整型参数都是向右对齐的,因此被调用方可忽略寄存器的高位,只访问所需的寄存器部分。
.text:0000000140001000 ; __int64 __fastcall func1(int, int, int, int, int e, int f)
.text:0000000140001000 func1 proc near
.text:0000000140001000
.text:0000000140001000 e= dword ptr 28h
.text:0000000140001000 f= dword ptr 30h
.text:0000000140001000
.text:0000000140001000 lea eax, [rcx+rdx] ; eax = 参数1 + 参数2
.text:0000000140001003 add eax, r8d ; eax = eax + 参数3
.text:0000000140001006 add eax, r9d ; eax = eax + 参数4
.text:0000000140001009 add eax, [rsp+e] ; eax = eax + 参数5
.text:000000014000100D add eax, [rsp+f] ; eax = eax + 参数6
.text:0000000140001011 retn
.text:0000000140001011
.text:0000000140001011 func1 endp
.text:0000000140001020 sub rsp, 38h
.text:0000000140001024 mov edx, 2 ; int 参数2
.text:0000000140001029 mov dword ptr [rsp+28h], 6 ; int 参数6
.text:0000000140001031 mov dword ptr [rsp+20h], 5 ; int 参数5
.text:0000000140001039 lea r9d, [rdx+2] ; int 参数4
.text:000000014000103D lea r8d, [rdx+1] ; int 参数3
.text:0000000140001041 lea ecx, [rdx-1] ; int 参数1
.text:0000000140001044 call func1
.text:0000000140001044
.text:0000000140001049 add rsp, 38h
.text:000000014000104D retn
前 4 个参数中的所有浮点和双精度参数都在 XMM0
- XMM3
(具体视位置而定)中传递。
.text:0000000140001000 ; float __fastcall func2(float, long double, float, long double, float, float)
.text:0000000140001000 func2 proc near
.text:0000000140001000
.text:0000000140001000 e= dword ptr 28h
.text:0000000140001000 f= dword ptr 30h
.text:0000000140001000
.text:0000000140001000 xorps xmm4, xmm4
.text:0000000140001003 cvtss2sd xmm4, xmm0 ; xmm4 = (double)参数1
.text:0000000140001007 movss xmm0, [rsp+e] ; xmm0 = 参数5
.text:000000014000100D addsd xmm4, xmm1 ; xmm4 = xmm4 + 参数2 (参数1 + 参数2)
.text:0000000140001011 xorps xmm1, xmm1
.text:0000000140001014 cvtss2sd xmm1, xmm2 ; xmm1 = (double)参数3
.text:0000000140001018 cvtps2pd xmm0, xmm0 ; xmm0 = (double)参数5
.text:000000014000101B addsd xmm4, xmm1 ; xmm4 = xmm4 + 参数3 (参数1 + 参数2 + 参数3)
.text:000000014000101F movss xmm1, [rsp+f]
.text:0000000140001025 cvtps2pd xmm1, xmm1 ; xmm1 = (double)参数6
.text:0000000140001028 addsd xmm4, xmm3 ; xmm4 = xmm4 + 参数4 (参数1 + 参数2 + 参数3 + 参数4)
.text:000000014000102C addsd xmm4, xmm0 ; xmm4 = xmm4 + 参数5 (参数1 + 参数2 + 参数3 + 参数4 + 参数5)
.text:0000000140001030 addsd xmm4, xmm1 ; xmm4 = xmm4 + 参数6 (参数1 + 参数2 + 参数3 + 参数4 + 参数5 + 参数6)
.text:0000000140001034 cvtpd2ps xmm0, xmm4 ; xmm0 = (float) 参数4
.text:0000000140001038 retn
.text:0000000140001038
.text:0000000140001038 func2 endp
.text:0000000140001040 sub rsp, 38h
.text:0000000140001044 movss xmm1, cs:__real@40a00000
.text:000000014000104C movss xmm0, cs:__real@40c00000
.text:0000000140001054 movsd xmm3, cs:__real@4010000000000000 ; long double 参数4
.text:000000014000105C movss xmm2, cs:__real@40400000 ; float 参数3
.text:0000000140001064 movss dword ptr [rsp+28h], xmm0 ; float 参数6
.text:000000014000106A movss xmm0, cs:__real@3f800000 ; float 参数1
.text:0000000140001072 movss dword ptr [rsp+20h], xmm1 ; float 参数5
.text:0000000140001078 movsd xmm1, cs:__real@4000000000000000 ; long double 参数2
.text:0000000140001080 call func2
.text:0000000140001080
.text:0000000140001085 cvttss2si eax, xmm0
.text:0000000140001089 add rsp, 38h
.text:000000014000108D retn
浮点数和整数混合传参,参数会放到对应的整数或浮点寄存器中。例如 fun(int, float, int, float)
函数对应参数会依次放到 ecx
,xmm1
,r8d
,xmm3
寄存器中。
.text:0000000140001000 ; float __fastcall func3(int, long double, int, float, int, float)
.text:0000000140001000 func3 proc near
.text:0000000140001000
.text:0000000140001000 e= dword ptr 28h
.text:0000000140001000 f= dword ptr 30h
.text:0000000140001000
.text:0000000140001000 xorps xmm2, xmm2
.text:0000000140001003 xorps xmm0, xmm0
.text:0000000140001006 cvtsi2sd xmm2, ecx ; xmm2 = (double)参数1
.text:000000014000100A cvtsi2sd xmm0, r8d ; xmm0 = (double)参数3
.text:000000014000100F addsd xmm2, xmm1 ; xmm2 = xmm2 + 参数1 (参数1 + 参数2)
.text:0000000140001013 xorps xmm1, xmm1
.text:0000000140001016 cvtss2sd xmm1, xmm3 ; xmm1 = (double) 参数4
.text:000000014000101A addsd xmm2, xmm0 ; xmm2 = xmm2 + 参数3 (参数1 + 参数2 + 参数3)
.text:000000014000101E xorps xmm0, xmm0
.text:0000000140001021 cvtsi2sd xmm0, [rsp+e] ; xmm0 = (double)参数5
.text:0000000140001027 addsd xmm2, xmm1 ; xmm2 = xmm2 + 参数4 (参数1 + 参数2 + 参数3 + 参数4)
.text:000000014000102B movss xmm1, [rsp+f] ; xmm1 = 参数6
.text:0000000140001031 cvtps2pd xmm1, xmm1
.text:0000000140001034 addsd xmm2, xmm0 ; xmm2 = xmm2 + xmm0 (参数1 + 参数2 + 参数3 + 参数4 + 参数5)
.text:0000000140001038 addsd xmm2, xmm1 ; xmm2 = xmm2 + xmm1 (参数1 + 参数2 + 参数3 + 参数4 + 参数5 + 参数6)
.text:000000014000103C cvtpd2ps xmm0, xmm2 ; xmm0 = (float)xmm2
.text:0000000140001040 retn
.text:0000000140001040
.text:0000000140001040 func3 endp
.text:0000000140001050 sub rsp, 38h
.text:0000000140001054 movss xmm0, cs:__real@40c00000
.text:000000014000105C mov r8d, 3 ; int 参数3
.text:0000000140001062 movss xmm3, cs:__real@40800000 ; float 参数4
.text:000000014000106A movsd xmm1, cs:__real@4000000000000000 ; long double 参数2
.text:0000000140001072 movss dword ptr [rsp+28h], xmm0 ; float 参数6
.text:0000000140001078 lea ecx, [r8-2] ; int 参数1
.text:000000014000107C mov dword ptr [rsp+20h], 5 ; int 参数5
.text:0000000140001084 call func3
.text:0000000140001084
.text:0000000140001089 cvttss2si eax, xmm0
.text:000000014000108D add rsp, 38h
.text:0000000140001091 retn
__m128
类型、数组和字符串从不通过即时值传递。 而是将分配的内存的指针传递给被调用方。 大小为 8、16、32 或 64 位的结构和联合以及 __m64
类型作为相同大小的整数传递。 其他大小的结构或联合作为指针传递给调用方分配的内存。 对于这些作为指针传递的聚合类型(包括 __m128
),调用方分配的临时内存必须是 16 字节对齐的。
例如下面这段代码:
#include<xmmintrin.h>
struct c {
float a;
float b;
};
double func4(__m64 a, __m128 b, struct c c, float d, __m128 e, __m128 f){
return a.m64_f32[0] + b.m128_f32[0] + c.b + d + e.m128_i16[2] + f.m128_i8[4];
}
int main() {
return func4((__m64){1}, (__m128){2}, (struct c){3}, 4, (__m128){5}, (__m128){6});
}
根据 IDA 反编译代码可知 __m128
类型传的是地址,其余传的是值。
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int128 v4; // [rsp+40h] [rbp-48h] BYREF
__int128 v5; // [rsp+50h] [rbp-38h] BYREF
__int128 v6; // [rsp+60h] [rbp-28h] BYREF
v4 = _xmm;
v5 = _xmm;
v6 = _xmm;
return (int)func4((__m64)1i64, (__m128 *)&v6, (c)0x40400000i64, 4.0, (__m128 *)&v5, (__m128 *)&v4);
}
main
函数汇编如下:
.text:0000000140001050 mov r11, rsp
.text:0000000140001053 sub rsp, 88h
.text:000000014000105A mov rax, cs:__security_cookie
.text:0000000140001061 xor rax, rsp
.text:0000000140001064 mov [rsp+70h], rax
.text:0000000140001069 movaps xmm2, cs:__xmm@00000000000000000000000040c00000
.text:0000000140001070 lea rax, [r11-48h]
.text:0000000140001074 movaps xmm1, cs:__xmm@00000000000000000000000040a00000
.text:000000014000107B lea rdx, [r11-28h] ; 参数2 [rsp+0x60] 地址
.text:000000014000107F movaps xmm0, cs:__xmm@00000000000000000000000040000000
.text:0000000140001086 mov ecx, 1 ; 参数1
.text:000000014000108B movss xmm3, cs:__real@40800000 ; 参数4
.text:0000000140001093 mov [r11-60h], rax ; 参数6 将地址 [rsp+0x40] 存入 [rsp+0x28]
.text:0000000140001097 lea rax, [r11-38h]
.text:000000014000109B mov qword ptr [r11-58h], 40400000h
.text:00000001400010A3 mov r8, [r11-58h] ; 参数3
.text:00000001400010A7 mov [r11-68h], rax ; 参数5 将地址 [rsp+0x50] 存入 [rsp+0x20]
.text:00000001400010AB movaps xmmword ptr [rsp+40h], xmm2 ; 初始化参数6
.text:00000001400010B0 movaps xmmword ptr [rsp+50h], xmm1 ; 初始化参数5
.text:00000001400010B5 movaps xmmword ptr [rsp+60h], xmm0 ; 初始化参数2
.text:00000001400010BA call func4
.text:00000001400010BA
.text:00000001400010BF cvttsd2si eax, xmm0
.text:00000001400010C3 mov rcx, [rsp+88h+var_18]
.text:00000001400010C8 xor rcx, rsp ; StackCookie
.text:00000001400010CB call __security_check_cookie
.text:00000001400010CB
.text:00000001400010D0 add rsp, 88h
.text:00000001400010D7 retn
返回值
可以适应 64 位的标量返回值(包括 __m64
类型)是通过 RAX
返回的。
; __int64 func()
func proc near
mov eax, 114514h
retn
func endp
非标量类型(包括浮点类型、双精度类型和向量类型,例如 __m128
、__m128i
、__m128d
)以 XMM0
的形式返回。
; float func(...)
func proc near
movss xmm0, cs:__real@47dfa900
retn
func endp
用户定义的结构体如果不超过 64 位则放入 RAX
寄存器返回,调用方传入局部结构体变量的地址作为第一个参数,返回值也是该结构体地址。例如下面这段代码:
struct Struct {
int j, k, l; // Struct1 exceeds 64 bits.
};
struct Struct func(int a,int b,int c) {
return (struct Struct){a, b, c};
}
int main() {
return func(1,2,3).j;
}
反编译代码如下,可以看到 func
函数的第一个参数为结构体地址,而这个结构体是 main
函数的一个局部变量。
Struct *__fastcall func(Struct *s, int a, int b, int c)
{
Struct *ret; // rax
s->j = a;
ret = s;
s->k = b;
s->l = c;
return ret;
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
Struct s; // [rsp+20h] [rbp-18h] BYREF
return func(&s, 1, 2, 3)->j;
}
具体汇编代码如下:
.text:0000000140001000 ; Struct *__fastcall func(Struct *s, int a, int b, int c)
.text:0000000140001000 func proc near
.text:0000000140001000 mov [rcx], edx
.text:0000000140001002 mov rax, rcx
.text:0000000140001005 mov [rcx+4], r8d
.text:0000000140001009 mov [rcx+8], r9d
.text:000000014000100D retn
.text:000000014000100D
.text:000000014000100D func endp
.text:0000000140001010 sub rsp, 38h
.text:0000000140001014 mov edx, 1 ; int
.text:0000000140001019 lea rcx, [rsp+20h] ; result
.text:000000014000101E lea r9d, [rdx+2] ; int
.text:0000000140001022 lea r8d, [rdx+1] ; int
.text:0000000140001026 call func
.text:0000000140001026
.text:000000014000102B mov eax, [rax]
.text:000000014000102D add rsp, 38h
.text:0000000140001031 retn
变量
局部变量
- x64 程度的函数中的局部变量大概率会被优化掉或者使用寄存器存储。
- 如果局部变量存储在栈上,为了保留函数的影子空间,一般会存放在
[rsp+20h]
以上的位置。
全局变量
示例代码如下(为了便于观察去掉了全局变量的C++名称修饰):
#include<random>
extern "C" {
int x;
int y = 1;
int z = (srand(time(NULL)), rand());
}
int main() {
printf("%d %d %d\n", x, y, z);
return 0;
}
我们可以看到全局变量 x
,y
,z
全部定义在了 .data
段:
.data:0000000140021000 y dd 1
.data:0000000140021AC0 x dd 0
.data:0000000140021AC4 z dd 0
.text:0000000140001090 sub rsp, 28h
.text:0000000140001094 mov r9d, cs:z
.text:000000014000109B lea rcx, _Format ; "%d %d %d\n"
.text:00000001400010A2 mov r8d, cs:y
.text:00000001400010A9 mov edx, cs:x
.text:00000001400010AF call printf
.text:00000001400010AF
.text:00000001400010B4 xor eax, eax
.text:00000001400010B6 add rsp, 28h
.text:00000001400010BA retn
其中 z
变量被 dynamic_initializer_for__z__
函数初始化:
int dynamic_initializer_for__z__()
{
unsigned int v0; // eax
int result; // eax
v0 = time(0i64);
srand(v0);
result = rand();
z = result;
return result;
}
在调用 main
函数之前 _scrt_common_main_seh
函数调用了 C++ 的初始化函数 initterm
,该函数会依次调用 _xc_a
和 _xc_z
之间的函数指针指向的函数,其中就包括 dynamic_initializer_for__z__
函数。
另外 C 的初始化函数是 initterm_e(_xi_a, _xi_z)
,该函数先于 initterm
函数调用。dynamic_initializer_for__z__
函数指针没有放到 _xi_a
和 _xi_z
之间也说明了 C 语言不支持全局变量 z
这种初始化方式。
// Calls each function in [first, last). [first, last) must be a valid range of
// function pointers. Each function is called, in order.
extern "C" void __cdecl _initterm(_PVFV* const first, _PVFV* const last)
{
for (_PVFV* it = first; it != last; ++it)
{
if (*it == nullptr)
continue;
(**it)();
}
}
// Calls each function in [first, last). [first, last) must be a valid range of
// function pointers. Each function must return zero on success, nonzero on
// failure. If any function returns nonzero, iteration stops immediately and
// the nonzero value is returned. Otherwise all functions are called and zero
// is returned.
//
// If a nonzero value is returned, it is expected to be one of the runtime error
// values (_RT_{NAME}, defined in the internal header files).
extern "C" int __cdecl _initterm_e(_PIFV* const first, _PIFV* const last)
{
for (_PIFV* it = first; it != last; ++it)
{
if (*it == nullptr)
continue;
int const result = (**it)();
if (result != 0)
return result;
}
return 0;
}
if ( initterm_e(_xi_a, _xi_z) )
return 0xFFi64;
initterm(_xc_a, _xc_z);
静态局部变量
首先如果静态全局变量的初始值为常量则等价为已初始化的全局变量。
如果初值不为常量会设置标志位来确保静态局部变量只被初始化一次。
实例代码:
int main(int argc) {
static int x = argc;
static int y = argc;
return (int) &x + (int) &y;
}
反编译代码:
.data:000000014001A000 _Init_global_epoch dd 80000000h
.data:000000014001AA80 x dd 0
.data:000000014001AA84 $TSS0 dd 0
.data:000000014001AA88 y dd 0
.data:000000014001AA8C $TSS1 dd 0
void __fastcall Init_thread_header(int *pOnce)
{
AcquireSRWLockExclusive(&g_tss_srw);
while ( 1 )
{
if ( !*pOnce )
{
*pOnce = -1;
goto LABEL_7;
}
if ( *pOnce != -1 )
break;
SleepConditionVariableSRW(&g_tss_cv, &g_tss_srw, 0xFFFFFFFF, 0);
}
*(_DWORD *)(*((_QWORD *)NtCurrentTeb()->Reserved1[11] + tls_index) + 4i64) = Init_global_epoch;
LABEL_7:
ReleaseSRWLockExclusive(&g_tss_srw);
}
void __fastcall Init_thread_footer(int *pOnce)
{
__int64 v2; // rdx
AcquireSRWLockExclusive(&g_tss_srw);
v2 = tls_index;
*pOnce = ++Init_global_epoch;
*(_DWORD *)(*((_QWORD *)NtCurrentTeb()->Reserved1[11] + v2) + 4i64) = Init_global_epoch;
ReleaseSRWLockExclusive(&g_tss_srw);
WakeAllConditionVariable(&g_tss_cv);
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
_DWORD *v4; // rdi
v4 = (_DWORD *)(*((_QWORD *)NtCurrentTeb()->Reserved1[11] + tls_index) + 4i64);
if ( _TSS0 > *v4 )
{
Init_thread_header(&_TSS0);
if ( _TSS0 == -1 )
{
x = argc;
Init_thread_footer(&_TSS0);
}
}
if ( _TSS1 > *v4 )
{
Init_thread_header(&_TSS1);
if ( _TSS1 == -1 )
{
y = argc;
Init_thread_footer(&_TSS1);
}
}
return (unsigned int)&x + (unsigned int)&y;
}