- 查看 C++ ASM
- 在线 C++ 反汇编
- g++ -S
- VS 调试时断点查看反汇编信息
- VS 在项目属性的文件输出源码+汇编
- Debug 下的:[ProjectName].asm
- Release 下的:[ProjectName].asm
- VS 调试数据
- 模块
- 进程
- 内存
- 寄存器
- 在内存中的一些字符表信息
- 编译出来的ASM文件通常包含的数据
- 基本分段/分块
- 反汇编实例
- Debug下的反汇编
- Release下的反汇编
- 删除 JMC 汇编检测代码 - CheckForJustMyCode
- 栈帧
- 总结
- 栈
- 栈帧
- EBP, ESP, EIP
- 调用函数前
- 调用函数内
- 其他常用寄存器 EAX
- call 指令
- ret 指令
- ret 后无数值
- ret 后有数值的
- 调用函数末端-返回-清理栈
- _stdcall
- _cdecl
- References
学习目的:
- 便于理解部分 shader 反汇编 后的代码查看
- 便于处理部分业务逻辑层的算法、逻辑优化等
- 如果未来有空,我会尝试自己写一个新的语言出来玩玩(但也就是提供具体的语法,逻辑运算,函数调用等功能的编译器 就够了,深入开发需要很巨量工作量。而且如果还加上预编译器、链接器、编译器的代码优化,那就麻烦了。因为一门语言要活跃起来需要软件生态支持,需要巨量的时间去实现各种现成库、调式工具等,来给使用者提高开发效率,否则没有意义,但如果是为了学习原理去重新一个简单的还是可以的。)
查看C++的ASM有好几种方式,不同的编译器,不同的版本,不同的ASM风格编译出来都不一样(汇编语法风格:有:Intel,AT&T的)
在线 C++ 反汇编https://godbolt.org/
有这么一段C++程序:
int main()
{
int a = 1;
int b = -1;
return a + b;
}
编译:g++ -S .\a.cpp
,得到:a.s 文件:
.file "a.cpp"
.text
.def ___main; .scl 2; .type 32; .endef
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $16, %esp
call ___main
movl $1, 12(%esp)
movl $2, 8(%esp)
movl 12(%esp), %edx
movl 8(%esp), %eax
addl %edx, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE0:
.ident "GCC: (MinGW.org GCC Build-20200227-1) 9.2.0"
VS 调试时断点查看反汇编信息
VS 下有好几种方法可以查看 MSVC 的编译器 CL.exe 编译出来的 C++ 汇编代码。
有这么一段C++程序:
int main()
{
int a = 1;
int b = -1;
return a + b;
}
中断点后,在菜单栏选择:调试->窗口->反汇编(Ctrl+Alt+D) 反汇编结果
--- D:\jave\Work Files\CPP\dasm\DASM\DASM\DASM.cpp -----------------------------
1: int main()
2: {
00AC1700 push ebp
00AC1701 mov ebp,esp
00AC1703 sub esp,0D8h
00AC1709 push ebx
00AC170A push esi
00AC170B push edi
00AC170C lea edi,[ebp-0D8h]
00AC1712 mov ecx,36h
00AC1717 mov eax,0CCCCCCCCh
00AC171C rep stos dword ptr es:[edi]
00AC171E mov ecx,offset _3D4FA793_DASM@cpp (0ACC000h)
00AC1723 call @__CheckForDebuggerJustMyCode@4 (0AC1208h)
3: int a = 1;
00AC1728 mov dword ptr [a],1
4: int b = -1;
00AC172F mov dword ptr [b],0FFFFFFFFh
5: return a + b;
00AC1736 mov eax,dword ptr [a]
00AC1739 add eax,dword ptr [b]
6: }
00AC173C pop edi
6: }
00AC173D pop esi
00AC173E pop ebx
00AC173F add esp,0D8h
00AC1745 cmp ebp,esp
00AC1747 call __RTC_CheckEsp (0AC1212h)
00AC174C mov esp,ebp
00AC174E pop ebp
00AC174F ret
VS 在项目属性的文件输出源码+汇编
项目属性->配置属性->C/C+±>输出文件->汇编程序输出->带源代码的程序集(/FAs)
注意 Debug、Release 下的项目属性是分开调整的。 接下来直接编译编译生成项目,这里我是在Debug下测试的,所以在Debug目录下可以看到有Dasm.asm文件,如下:
CPP的源码还是和上面的一样,查看一样 DASM.asm的汇编源码是怎么样的:
Debug 下的:[ProjectName].asm; Listing generated by Microsoft (R) Optimizing Compiler Version 19.25.28610.4
TITLE D:\jave\Work Files\CPP\dasm\DASM\DASM\DASM.cpp
.686P
.XMM
include listing.inc
.model flat
INCLUDELIB MSVCRTD
INCLUDELIB OLDNAMES
msvcjmc SEGMENT
__3D4FA793_DASM@cpp DB 01H
msvcjmc ENDS
PUBLIC _main
PUBLIC __JustMyCode_Default
EXTRN @__CheckForDebuggerJustMyCode@4:PROC
EXTRN __RTC_CheckEsp:PROC
EXTRN __RTC_InitBase:PROC
EXTRN __RTC_Shutdown:PROC
; COMDAT rtc$TMZ
rtc$TMZ SEGMENT
__RTC_Shutdown.rtc$TMZ DD FLAT:__RTC_Shutdown
rtc$TMZ ENDS
; COMDAT rtc$IMZ
rtc$IMZ SEGMENT
__RTC_InitBase.rtc$IMZ DD FLAT:__RTC_InitBase
rtc$IMZ ENDS
; Function compile flags: /Odt
; COMDAT __JustMyCode_Default
_TEXT SEGMENT
__JustMyCode_Default PROC ; COMDAT
push ebp
mov ebp, esp
pop ebp
ret 0
__JustMyCode_Default ENDP
_TEXT ENDS
; Function compile flags: /Odtp /RTCsu /ZI
; File D:\jave\Work Files\CPP\dasm\DASM\DASM\DASM.cpp
; COMDAT _main
_TEXT SEGMENT
_b$ = -20 ; size = 4
_a$ = -8 ; size = 4
_main PROC ; COMDAT
; 2 : {
push ebp
mov ebp, esp
sub esp, 216 ; 000000d8H
push ebx
push esi
push edi
lea edi, DWORD PTR [ebp-216]
mov ecx, 54 ; 00000036H
mov eax, -858993460 ; ccccccccH
rep stosd
mov ecx, OFFSET __3D4FA793_DASM@cpp
call @__CheckForDebuggerJustMyCode@4
; 3 : int a = 1;
mov DWORD PTR _a$[ebp], 1
; 4 : int b = -1;
mov DWORD PTR _b$[ebp], -1
; 5 : return a + b;
mov eax, DWORD PTR _a$[ebp]
add eax, DWORD PTR _b$[ebp]
; 6 : }
pop edi
pop esi
pop ebx
add esp, 216 ; 000000d8H
cmp ebp, esp
call __RTC_CheckEsp
mov esp, ebp
pop ebp
ret 0
_main ENDP
_TEXT ENDS
END
上面Debug下的调试信息太多了,再调整到Release,生成看看:
Release 下的:[ProjectName].asm; Listing generated by Microsoft (R) Optimizing Compiler Version 19.25.28610.4
TITLE d:\jave\work files\cpp\dasm\dasm\dasm\dasm.cpp
.686P
.XMM
include listing.inc
.model flat
INCLUDELIB OLDNAMES
EXTRN @__security_check_cookie@4:PROC
PUBLIC _main
; Function compile flags: /Ogtp
; File D:\jave\Work Files\CPP\dasm\DASM\DASM\DASM.cpp
; COMDAT _main
_TEXT SEGMENT
_main PROC ; COMDAT
; 3 : int a = 1;
; 4 : int b = -1;
; 5 : return a + b;
xor eax, eax
; 6 : }
ret 0
_main ENDP
_TEXT ENDS
END
可以看到,Release 下,因:代码优化 后,return 之前的一些代码,压根没生成对应的汇编处理,因为MSVC cl.exe 下编译的Main函数是否返回值,都无关紧要的。
当然,如果你仍然想在Release “比较干净”的代码上查看asm,可以打开项目配置:C/C++->优化
关闭优化 /Od (Od的意思:Optimization Disabled ),这样就可以查看到不优化而不会删除了无用逻辑后的汇编是怎么样的。
调试->窗口-> 都可以查看:
- 模块:该进程以加载模块(都是一些已经加载的哪些库)
- 进程:当前相关进程
- 内存:当前机器内存(在配合反汇编调试、查看数据是很有用的)
- 反汇编:就是我们上面提到的
- 寄存器:可以查看当前寄存器的数据状态(在配合反汇编调试、查看数据是很有用的)
可查看到使用了的各种动态链接的文件
这里的内存查看应该是此程序进程的逻辑内存地址(即:进程内存段+逻辑内存地址,来寻址的)
寄存器 寄存器中,可以查看CPU算术寄存器,段寄存器,浮点寄存器,等。
如果我们在程序中列出一些常量字符串,然后我们还可以用附加该.exe程序进程后调式(就是断点调试该程序),查看内存可以看到一些字符表的信息,如下,我定位查找在程序中明文使用了Hello world!
,然后根据字符串变量指向的常量地址,再通过上面介绍的内存来定位(Hello world!在下面内容中第三行),该字符的地址的前前后后也查找到了其他的一些字符串表的内容,这些字符串应该是编译、链接、合并其他的程序内带上的字符表。
Stack around the variable '.' was corrupted.
The variable '..' is being used without being initialized
Hello world!
The value of ESP was not properly saved across a function call.
This is usually a result of calling a function declared with one calling convention with a function pointer declared with a different calling onvention
A cast to a smaller data type has caused a loss of data. If this was intentional, you should mask the source of the cast with the appropriate bitmask.
For example:
char c = (i & 0xFF);
Changing the code in this way will not affect the quality of the resulting optimized code
Stack memory was corrupted
A local variable was used before it was initialized
Stack memory around _alloca was corrupted
Unknown Runtime Check Error.
R.u.n.t.i.m.e. .C.h.e.c.k. .E.r.r.o.r.......
U.n.a.b.l.e. .t.o. .d.i.s.p.l.a.y. .R.T.C. .M.e.s.s.a.g.e.
R.u.n.-.T.i.m.e. .C.h.e.c.k. .F.a.i.l.u.r.e.
#.%.d. .-. .%.s
Unknown Filename
Unknown Module Name
Run-Time Check Failure #%d - %s
Stack corrupted near unknown variable
%.2X
Stack area around _alloca memory reserved by this function is corrupted
Data:
Allocation number within this function:
Size:
Address:
Stack area around _alloca memory reserved by this function is corrupted
A variable is being used without being initialized tack pointer corruption
Cast to smaller type causing loss of data
Stack memory corruption
Local variable used before initialization
Stack around _alloca corrupted..
编译出来的ASM文件通常包含的数据
了解我们编译出来的到底是什么文件,首先先了解,我们平时用gcc/g++或是MSVC cl等编译器来编译源文件时,是怎么个步骤:
- Pre-Processing : 预编译的处理,就是对一些
#define
开头的宏展开,#include
文件引入到对应位置,或是预编译指令处理:#if #ifdef #ifndef #elif #else #endif
等,但是#pragma
会保留不处理,这是编译器要处理的编译指令,GCC中对应:g++ -E a.cpp -i out.txt
,或是g++ -E a.cpp > out.txt
- Compilation : 编译,对预编译后的所有源文件开是编译处理:词法分析、语法分析、语义分析、生成中间代码、中间代码优化(可选),GCC中对应:
g++ -S a.app
生成 a.s,这时带有预编译、与编译的处理的 - Assembly : 汇编编译,对中间代码优化,GCC中对应::
g++ -c a.app
生成 a.o - Links:将目标文件(.o,.lib)或是可执行文件(PE/ELF之类的可执行文件)或是动态链接文件(DLL)链接处理。静态链接会生成新的目标文件,将各个子目标文件的段数据合并,调整各个段数据(.text:代码或是只读数据, .data:静态或是全局数据, .bss:未初始化的静态或全局数据, .symtable:变量或是函数的symbol table符号表, and so on)的虚拟内存地址(VMA:Virtual Memory Address)与段大小(Size),还有程序入口指令地址,等。
编译出来的文件与Window PE(Portable Executable:便携式可执行的),Linux ELF(Executable Linkable Format:可执行可链接的格式)有几分相似,他们都是COFF(Common File Format:公共文件格式)格式之一。
基本分段/分块- 代码区 - 在反汇编代码中的:
_TEXT SEGMENT ... _TEXT ENDS
之间 - 数据区
- 全局已初始化区:
_DATA SEGMENT ... _DATA ENDS
之间 - 全局未初始化区:
_BSS SEGMENT ... _BSS ENDS
之间 - 常量区:
CONST SEGMENT ... CONST ENDS
之间
- 全局已初始化区:
以上的这些定义区块,可以在 VSC 或是 Sublime 在使用正则快速搜索,写了个正则为:
- 通用格式:
(\w*)\W*?SEGMENT[\w\W]*?\1\W*?ENDS
- 查找:
_TEXT
代码区块:(_TEXT)\W*?SEGMENT[\w\W]*?\1\W*?ENDS
- 查找:
_DATA
全局已初始化区:(_DATA)\W*?SEGMENT[\w\W]*?\1\W*?ENDS
- 查找:
_BSS
全局未初始化区:(_BSS)\W*?SEGMENT[\w\W]*?\1\W*?ENDS
- 查找:
CONST
常量区:(CONST)\W*?SEGMENT[\w\W]*?\1\W*?ENDS
- 查找:
但是上面的正则在 Release 搜索不完整,因为 Release 下有些区块标记会的 XXX SEGMENT会忽略掉,在下面列出的 Release 的反汇编可以看到。但也不影响我们理解这些概念。
这些区块或叫:分段数据,每一个编译出来的文件都会有的,然后他们不同文件最终会链接合并(能直接合并在一块的都是静态链接库)为一个:目标文件(库、可执行文件等)
所以目标文件一般是多个静态库其他的编译文件合并的文件,动态链接就不一样了,它是编译时有检测,运行时才重定位使用到的动态库:函数或数据的地址
反汇编实例就已字符常量区为例,我们用一段示例C++程序:
// jave.lin - test for deassembly code
int s_i = 999; // static_int
int s_i1 = 0xff001122;
float s_f_ui; // static_float, un initialize 未初始化
float s_f1 = 0.888; // static_float
const int c_i = 1000; // const int,不在常量区
const float c_f = 0.333; // const float
const char* c_str_ui; // const char*, un initialize
const char* c_str = "This is Global const str."; // const char*
int main()
{
const char *l_c_str_helloworld = "Hello world!"; // local const string,明文编写字符串比较特别,通过反汇编可以看到,右的值都是常量
const char* l_c_str_second = "This is Seconds const str."; // local const string,明文编写字符串比较特别,通过反汇编可以看到,右的值都是常量
char l_d_str_thrid[50] = "This is Thrid dynamic str."; // local dynamic string,明文编写字符串比较特别,通过反汇编可以看到,右的值都是常量
l_d_str_thrid[0] = '#'; //明文编写字符不会有常量,因为当做一个byte字节数据来处理
int l_i = s_i; // local int = static int
int l_i1 = s_i1; // local int = static int
int l_f_ui = s_f_ui; // local float = static float
int l_f1 = s_f1; // local float = static float
const int l_c_i = c_i; // local const int = const int,通过反汇编可以看到,c_i被当做立即数使用了,c_i不存在常量区
//const int* p_l_c_i = &c_i; // 但是如果有代码使用到c_i的地址,c_i在常量区就会有储存,这是编译器的优化
const float l_c_f = c_f; // local const float = const float,通过反汇编可以看到,c_f在常量区有存储,这与const int是有区别的
const char* l_c_str_ui = c_str_ui; // local const char* = const char*
const char* l_c_str = c_str; // local const char* = const char*
return 0;
}
Debug下的反汇编
下面可以查看刚刚上面说的对应区块内容
; Listing generated by Microsoft (R) Optimizing Compiler Version 19.25.28614.0
TITLE D:\jave\Work Files\CPP\dasm\DASM\DASM\DASM.cpp
.686P
.XMM
include listing.inc
.model flat
INCLUDELIB MSVCRTD
INCLUDELIB OLDNAMES
PUBLIC ?s_i@@3HA ; s_i
PUBLIC ?s_i1@@3HA ; s_i1
PUBLIC ?s_f_ui@@3MA ; s_f_ui
PUBLIC ?s_f1@@3MA ; s_f1
PUBLIC ?c_str_ui@@3PBDB ; c_str_ui
PUBLIC ?c_str@@3PBDB ; c_str
PUBLIC ??_C@_0BK@OEHJENGO@This?5is?5Global?5const?5str?4@ ; `string'
_BSS SEGMENT
?s_f_ui@@3MA DD 01H DUP (?) ; s_f_ui
?c_str_ui@@3PBDB DD 01H DUP (?) ; c_str_ui
_BSS ENDS
msvcjmc SEGMENT
__3D4FA793_DASM@cpp DB 01H
msvcjmc ENDS
; COMDAT ??_C@_0BK@OEHJENGO@This?5is?5Global?5const?5str?4@
CONST SEGMENT
??_C@_0BK@OEHJENGO@This?5is?5Global?5const?5str?4@ DB 'This is Global con'
DB 'st str.', 00H ; `string'
CONST ENDS
_DATA SEGMENT
?s_i@@3HA DD 03e7H ; s_i
?s_i1@@3HA DD 0ff001122H ; s_i1
?s_f1@@3MA DD 03f6353f8r ; 0.888 ; s_f1
?c_str@@3PBDB DD FLAT:??_C@_0BK@OEHJENGO@This?5is?5Global?5const?5str?4@ ; c_str
_DATA ENDS
PUBLIC _main
PUBLIC __JustMyCode_Default
PUBLIC ??_C@_0N@KNIDPCKA@Hello?5world?$CB@ ; `string'
PUBLIC ??_C@_0BL@LGABGNAE@This?5is?5Seconds?5const?5str?4@ ; `string'
PUBLIC ??_C@_0BL@DIJMBJEP@This?5is?5Thrid?5dynamic?5str?4@ ; `string'
PUBLIC __real@3eaa7efa
EXTRN @_RTC_CheckStackVars@8:PROC
EXTRN @__CheckForDebuggerJustMyCode@4:PROC
EXTRN @__security_check_cookie@4:PROC
EXTRN __RTC_CheckEsp:PROC
EXTRN __RTC_InitBase:PROC
EXTRN __RTC_Shutdown:PROC
EXTRN ___security_cookie:DWORD
EXTRN __fltused:DWORD
; COMDAT __real@3eaa7efa
CONST SEGMENT
__real@3eaa7efa DD 03eaa7efar ; 0.333
CONST ENDS
; COMDAT rtc$TMZ
rtc$TMZ SEGMENT
__RTC_Shutdown.rtc$TMZ DD FLAT:__RTC_Shutdown
rtc$TMZ ENDS
; COMDAT rtc$IMZ
rtc$IMZ SEGMENT
__RTC_InitBase.rtc$IMZ DD FLAT:__RTC_InitBase
rtc$IMZ ENDS
; COMDAT ??_C@_0BL@DIJMBJEP@This?5is?5Thrid?5dynamic?5str?4@
CONST SEGMENT
??_C@_0BL@DIJMBJEP@This?5is?5Thrid?5dynamic?5str?4@ DB 'This is Thrid dyn'
DB 'amic str.', 00H ; `string'
CONST ENDS
; COMDAT ??_C@_0BL@LGABGNAE@This?5is?5Seconds?5const?5str?4@
CONST SEGMENT
??_C@_0BL@LGABGNAE@This?5is?5Seconds?5const?5str?4@ DB 'This is Seconds c'
DB 'onst str.', 00H ; `string'
CONST ENDS
; COMDAT ??_C@_0N@KNIDPCKA@Hello?5world?$CB@
CONST SEGMENT
??_C@_0N@KNIDPCKA@Hello?5world?$CB@ DB 'Hello world!', 00H ; `string'
CONST ENDS
; Function compile flags: /Odt
; COMDAT __JustMyCode_Default
_TEXT SEGMENT
__JustMyCode_Default PROC ; COMDAT
push ebp
mov ebp, esp
pop ebp
ret 0
__JustMyCode_Default ENDP
_TEXT ENDS
; Function compile flags: /Odtp /RTCsu /ZI
; File D:\jave\Work Files\CPP\dasm\DASM\DASM\DASM.cpp
; COMDAT _main
_TEXT SEGMENT
_l_c_str$ = -180 ; size = 4
_l_c_str_ui$ = -168 ; size = 4
_l_c_f$ = -156 ; size = 4
_l_c_i$ = -144 ; size = 4
_l_f1$ = -132 ; size = 4
_l_f_ui$ = -120 ; size = 4
_l_i1$ = -108 ; size = 4
_l_i$ = -96 ; size = 4
_l_d_str_thrid$ = -84 ; size = 50
_l_c_str_second$ = -24 ; size = 4
_l_c_str_helloworld$ = -12 ; size = 4
__$ArrayPad$ = -4 ; size = 4
_main PROC ; COMDAT
; 13 : {
push ebp
mov ebp, esp
sub esp, 376 ; 00000178H
push ebx
push esi
push edi
lea edi, DWORD PTR [ebp-376]
mov ecx, 94 ; 0000005eH
mov eax, -858993460 ; ccccccccH
rep stosd
mov eax, DWORD PTR ___security_cookie
xor eax, ebp
mov DWORD PTR __$ArrayPad$[ebp], eax
mov ecx, OFFSET __3D4FA793_DASM@cpp
call @__CheckForDebuggerJustMyCode@4
; 14 : const char *l_c_str_helloworld = "Hello world!"; // local const string,明文编写字符串比较特别,通过反汇编可以看到,右的值都是常量
mov DWORD PTR _l_c_str_helloworld$[ebp], OFFSET ??_C@_0N@KNIDPCKA@Hello?5world?$CB@
; 15 : const char* l_c_str_second = "This is Seconds const str."; // local const string,明文编写字符串比较特别,通过反汇编可以看到,右的值都是常量
mov DWORD PTR _l_c_str_second$[ebp], OFFSET ??_C@_0BL@LGABGNAE@This?5is?5Seconds?5const?5str?4@
; 16 : char l_d_str_thrid[50] = "This is Thrid dynamic str."; // local dynamic string,明文编写字符串比较特别,通过反汇编可以看到,右的值都是常量
mov ecx, 6
mov esi, OFFSET ??_C@_0BL@DIJMBJEP@This?5is?5Thrid?5dynamic?5str?4@
lea edi, DWORD PTR _l_d_str_thrid$[ebp]
rep movsd
movsw
movsb
xor eax, eax
mov DWORD PTR _l_d_str_thrid$[ebp+27], eax
mov DWORD PTR _l_d_str_thrid$[ebp+31], eax
mov DWORD PTR _l_d_str_thrid$[ebp+35], eax
mov DWORD PTR _l_d_str_thrid$[ebp+39], eax
mov DWORD PTR _l_d_str_thrid$[ebp+43], eax
mov WORD PTR _l_d_str_thrid$[ebp+47], ax
mov BYTE PTR _l_d_str_thrid$[ebp+49], al
; 17 :
; 18 : l_d_str_thrid[0] = '#'; //明文编写字符不会有常量,因为当做一个byte字节数据来处理
mov eax, 1
imul ecx, eax, 0
mov BYTE PTR _l_d_str_thrid$[ebp+ecx], 35 ; 00000023H
; 19 :
; 20 : int l_i = s_i; // local int = static int
mov eax, DWORD PTR ?s_i@@3HA ; s_i
mov DWORD PTR _l_i$[ebp], eax
; 21 : int l_i1 = s_i1; // local int = static int
mov eax, DWORD PTR ?s_i1@@3HA ; s_i1
mov DWORD PTR _l_i1$[ebp], eax
; 22 : int l_f_ui = s_f_ui; // local float = static float
cvttss2si eax, DWORD PTR ?s_f_ui@@3MA
mov DWORD PTR _l_f_ui$[ebp], eax
; 23 : int l_f1 = s_f1; // local float = static float
cvttss2si eax, DWORD PTR ?s_f1@@3MA
mov DWORD PTR _l_f1$[ebp], eax
; 24 : const int l_c_i = c_i; // local const int = const int,通过反汇编可以看到,c_i被当做立即数使用了,c_i不存在常量区
mov DWORD PTR _l_c_i$[ebp], 1000 ; 000003e8H
; 25 : //const int* p_l_c_i = &c_i; // 但是如果有代码使用到c_i的地址,c_i在常量区就会有储存,这是编译器的优化
; 26 : const float l_c_f = c_f; // local const float = const float,通过反汇编可以看到,c_f在常量区有存储,这与const int是有区别的
movss xmm0, DWORD PTR __real@3eaa7efa
movss DWORD PTR _l_c_f$[ebp], xmm0
; 27 : const char* l_c_str_ui = c_str_ui; // local const char* = const char*
mov eax, DWORD PTR ?c_str_ui@@3PBDB ; c_str_ui
mov DWORD PTR _l_c_str_ui$[ebp], eax
; 28 : const char* l_c_str = c_str; // local const char* = const char*
mov eax, DWORD PTR ?c_str@@3PBDB ; c_str
mov DWORD PTR _l_c_str$[ebp], eax
; 29 :
; 30 : return 0;
xor eax, eax
; 31 : }
push edx
mov ecx, ebp
push eax
lea edx, DWORD PTR $LN5@main
call @_RTC_CheckStackVars@8
pop eax
pop edx
pop edi
pop esi
pop ebx
mov ecx, DWORD PTR __$ArrayPad$[ebp]
xor ecx, ebp
call @__security_check_cookie@4
add esp, 376 ; 00000178H
cmp ebp, esp
call __RTC_CheckEsp
mov esp, ebp
pop ebp
ret 0
npad 3
$LN5@main:
DD 1
DD $LN4@main
$LN4@main:
DD -84 ; ffffffacH
DD 50 ; 00000032H
DD $LN3@main
$LN3@main:
DB 108 ; 0000006cH
DB 95 ; 0000005fH
DB 100 ; 00000064H
DB 95 ; 0000005fH
DB 115 ; 00000073H
DB 116 ; 00000074H
DB 114 ; 00000072H
DB 95 ; 0000005fH
DB 116 ; 00000074H
DB 104 ; 00000068H
DB 114 ; 00000072H
DB 105 ; 00000069H
DB 100 ; 00000064H
DB 0
_main ENDP
_TEXT ENDS
END
Release下的反汇编
Release下的,有些XXX SEGMENT
头部标记没了
; Listing generated by Microsoft (R) Optimizing Compiler Version 19.25.28614.0
TITLE d:\jave\work files\cpp\dasm\dasm\dasm\dasm.cpp
.686P
.XMM
include listing.inc
.model flat
INCLUDELIB OLDNAMES
PUBLIC ??_C@_0BK@OEHJENGO@This?5is?5Global?5const?5str?4@ ; `string'
PUBLIC ??_C@_0N@KNIDPCKA@Hello?5world?$CB@ ; `string'
PUBLIC ??_C@_0BL@LGABGNAE@This?5is?5Seconds?5const?5str?4@ ; `string'
PUBLIC ??_C@_0BL@DIJMBJEP@This?5is?5Thrid?5dynamic?5str?4@ ; `string'
PUBLIC ?s_i1@@3HA ; s_i1
PUBLIC ?s_f_ui@@3MA ; s_f_ui
PUBLIC ?s_f1@@3MA ; s_f1
PUBLIC ?c_str@@3PBDB ; c_str
PUBLIC ?s_i@@3HA ; s_i
PUBLIC ?c_str_ui@@3PBDB ; c_str_ui
EXTRN @__security_check_cookie@4:PROC
?s_f_ui@@3MA DD 01H DUP (?) ; s_f_ui
?c_str_ui@@3PBDB DD 01H DUP (?) ; c_str_ui
_BSS ENDS
?s_i1@@3HA DD 0ff001122H ; s_i1
?s_f1@@3MA DD 03f6353f8r ; 0.888 ; s_f1
?c_str@@3PBDB DD FLAT:??_C@_0BK@OEHJENGO@This?5is?5Global?5const?5str?4@ ; c_str
?s_i@@3HA DD 03e7H ; s_i
CONST ENDS
; COMDAT ??_C@_0BL@DIJMBJEP@This?5is?5Thrid?5dynamic?5str?4@
CONST SEGMENT
??_C@_0BL@DIJMBJEP@This?5is?5Thrid?5dynamic?5str?4@ DB 'This is Thrid dyn'
DB 'amic str.', 00H ; `string'
CONST ENDS
; COMDAT ??_C@_0BL@LGABGNAE@This?5is?5Seconds?5const?5str?4@
CONST SEGMENT
??_C@_0BL@LGABGNAE@This?5is?5Seconds?5const?5str?4@ DB 'This is Seconds c'
DB 'onst str.', 00H ; `string'
CONST ENDS
; COMDAT ??_C@_0N@KNIDPCKA@Hello?5world?$CB@
CONST SEGMENT
??_C@_0N@KNIDPCKA@Hello?5world?$CB@ DB 'Hello world!', 00H ; `string'
CONST ENDS
; COMDAT ??_C@_0BK@OEHJENGO@This?5is?5Global?5const?5str?4@
CONST SEGMENT
??_C@_0BK@OEHJENGO@This?5is?5Global?5const?5str?4@ DB 'This is Global con'
DB 'st str.', 00H ; `string'
CONST ENDS
PUBLIC _main
EXTRN __fltused:DWORD
; Function compile flags: /Ogtp
; File D:\jave\Work Files\CPP\dasm\DASM\DASM\DASM.cpp
; COMDAT _main
_TEXT SEGMENT
_main PROC ; COMDAT
; 14 : const char *l_c_str_helloworld = "Hello world!"; // local const string,明文编写字符串比较特别,通过反汇编可以看到,右的值都是常量
; 15 : const char* l_c_str_second = "This is Seconds const str."; // local const string,明文编写字符串比较特别,通过反汇编可以看到,右的值都是常量
; 16 : char l_d_str_thrid[50] = "This is Thrid dynamic str."; // local dynamic string,明文编写字符串比较特别,通过反汇编可以看到,右的值都是常量
; 17 :
; 18 : l_d_str_thrid[0] = '#'; //明文编写字符不会有常量,因为当做一个byte字节数据来处理
; 19 :
; 20 : int l_i = s_i; // local int = static int
; 21 : int l_i1 = s_i1; // local int = static int
; 22 : int l_f_ui = s_f_ui; // local float = static float
; 23 : int l_f1 = s_f1; // local float = static float
; 24 : const int l_c_i = c_i; // local const int = const int,通过反汇编可以看到,c_i被当做立即数使用了,c_i不存在常量区
; 25 : //const int* p_l_c_i = &c_i; // 但是如果有代码使用到c_i的地址,c_i在常量区就会有储存,这是编译器的优化
; 26 : const float l_c_f = c_f; // local const float = const float,通过反汇编可以看到,c_f在常量区有存储,这与const int是有区别的
; 27 : const char* l_c_str_ui = c_str_ui; // local const char* = const char*
; 28 : const char* l_c_str = c_str; // local const char* = const char*
; 29 :
; 30 : return 0;
xor eax, eax
; 31 : }
ret 0
_main ENDP
_TEXT ENDS
END
Release下的逻辑代码都因为优化删除了。
但是一些全局的变量,和常量在生成的目标文件也是存在的。
删除 JMC 汇编检测代码 - CheckForJustMyCode编译出来的汇编代码代码可以看到每一个函数调用都有一段__CheckForDebuggerJustMyCode
,觉得碍眼也可以通过:项目->属性->配置属性->C/C+±>常规->支持仅我的代码调式:->选“否” 来关闭这些代码的调用。
上面就是简单介绍一些查看反汇编的内容
栈帧栈:stack 栈帧: stack frame
简单理解:栈帧 就是 栈 里头的 一帧数据。
有些简单或是底层、嵌入式的汇编程序编写,可能没有栈帧(反正就是不使用高级语言编写的程序,有可能一个函数都没调用,都是由基础指令处理的程序,即:没有call),但理论上可以通过一些类似一些 push, pop, mov, jmp
,等命令来控制BP(Based Pointer),SP(Stack Pointer),IP(Instruction Pointer)模拟。以此来实现类似的栈帧功能。
但要看懂高级语言的反汇编指令(有call指令调用的),需要了解:栈帧。
关于栈帧的理解可参考:
- 手把手教你栈溢出从入门到放弃(上)
- 手把手教你栈溢出从入门到放弃(下)
- 这个从入门到放弃的上、下篇看完基本对算是栈帧有了一定理解,推荐可以看看,图文都比较清晰
- 系统栈的工作原理
- 栈帧(Stack Frame)
- 百科 - 栈帧
- 汇编中EBP寄存器和ESP寄存器的区别
- 栈帧与函数调用过程
看完上面的一些零零散散的文章后,可总结
总结注意下面描述时,需要区别:栈,栈帧两个不同的概念
栈- 系统维护栈,这个栈指针内存对象假设声明为:
char *pStack;
。 - 与数据结构中的栈差不多的,都是后进先出(后push,先pop)栈内存大小:每个独立进程都有的一份运行需要的栈数据,通常是2M的大小
- 即:
char *pStack = malloc(2 * 1024 * 1024);
- 然后定义一个栈底指针:
char *pStackBased = pStack + (2 * 1024 * 1024);
- SBP(Stack Based Pointer) - 然后定义一个栈顶指针:
*char *pStackTop = pStack;
- ESP(与汇编的一样的名字) - 由上面的SBP,ESP指向的地址位置,就可以看出此栈与数据结构中的栈不太一样
push
时,栈顶pStackTop
的地址是减少的,在汇编里头一般可以看到:push eax
会是push 999
,类似下列伪代码:pop
时,栈顶pStackTop
的地址是增加的,在汇编里头一般可以看到:pop ebp
,类似下列伪代码:
- 即:
void push(const data_type data) {
*(pStackTop--) = data; // 先放置到sTackTop上,再偏移指针到下一个位置,注意这里的pStackTop是--的,因为这个系统栈的栈底在最高的地址
}
void pop(register_type ®ister_ref) {
register_ref = *(++pStackTop); // 先偏移到上一次栈顶,再取栈顶上存放的数据
}
- 如果我们的程序调用层次太多,就容易调用栈溢出(假设栈的基地址为:SBP(Stack Based Pointer),即我们前面假设的:
pStack
,溢出就是:(SBP - ESP) > (2 * 1024 * 1024)),常见的就是我们程序中的无限递归的BUG
- 栈帧是系统维护的内存中的一块内存,只不过它仅仅包含的是某个方法调用时的函数栈帧状态需要的数据,一个栈帧通常包含一些状态数据:EBP、ESP、EIP
- 栈帧的状态数据范围:就是EBP(高地址)~ESP(底地址)之间的内存数据
- EBP:Extend Based Pointer,一直指向的是当前栈帧的基地址,也叫栈帧的底,相对ESP来说,EBP是高地址,在整个函数调用的栈帧中是不变的,除非到准备弹出该栈帧时,会设置回上一个栈帧(父级调用函数)的EBP,
- ESP:Extend Stack Pointer,一直指向的是当前栈帧的顶,相对EBP来说,ESP是底地址,也是一直指向栈的栈顶(注意是栈的栈顶),栈的栈顶与栈帧的栈顶都是相同的,也就说ESP当前会一直指向栈顶,与栈帧顶,与前面介绍栈的ESP一样
push、pop
都会减少与增加ESP的地址。 - EIP:Extend Instruction Pointer,一直指向CPU当前准备需要执行的指令指针。asm 函数调用
call
其中就包含了对EIP定位到指定函数指令首地址(call functionAddress
包含两个操作等价于伪代码:(一)push (current eip + size(current cmd))
,(二)mov eip, functionAddress
)
所以一个栈帧的状态需要上面三个数据:EBP,ESP,EIP
。
那么父函数调用子函数,然后子函数执行完了之后如何恢复回父函数呢?
这句话也可以这么问:
弹出一个栈帧,如何将EBP,ESP,EIP
恢复到上一帧的这些数据呢?
答案就是:
在构建新的一个栈帧时,就存有上一帧(上一个函数)的这三个EBP,ESP,EIP
数据即可。
所以我们在反汇编代码中可以看到(不开启代码优化)每个函数调用前都用类似下面的码:
示例对应的C++源码:
#include
#include
//#include"my_funcs.h"
int
//_cdecl
_stdcall
add(int a, int b, int c)
{
int local1 = 0;
int local2 = 10;
int local3 = 1;
long long local4 = 2;
return a + b + c + local1 + local2;
}
int main() {
int a = 1;
int b = 2;
int c = 3;
int d = add(a, b, c);
printf("d = %d\n", d);
//d = multiply(b, d);
//printf("d = %d\n", d);
return c;
}
下面是反汇编后add函数前、中、后的处理
调用函数前add函数有三个参数,在调用前,都push到栈里,参数入栈的顺序是按:调用约定来决定的。调用约定同时也决定了函数栈帧在恢复到上一个栈帧是在 被调用 函数栈帧中恢复,还是在上一个调用 函数中的栈帧恢复。
调用函数内push ebp
先将上一个栈帧的ebp
备份到当前栈顶mov ebp, esp
将esp
寄存器的值赋值给ebp
寄存器,可理解为:将当前栈顶作为此栈帧的新的ebp
sub esp, 14h
将esp
寄存器的值减少14h
,可理解为:分配好此栈帧的内存大小,大小为十六进制的:14h
,对应十进制为:20
- EAX:Extend Accumulator X,Accumulator是累积、累加的意思,就是一般做运算用的,X代表他是一个通过寄存器的意思。用于栈帧弹出前,将该函数栈帧的返回值设置到EAX,以便恢复到上一个栈帧的下一条指令处理用(如果有返回值的话),下一条命令只要接收EAX寄存器的值就可以了。在反汇编的代码中除了常在在函数结尾返回值对EAX赋值处理,在一般的立即数赋值或是初始化都话看到有此寄存器的频繁使用
可参考:“The call instruction pushes the return address onto the stack then jumps to the destination.” 或是看我之前翻译
call functions-address
相当于
push eip+2 ; // 2 是不确定,不通编译器、或是不同编译平台目标的指令处理的下一条指令是不同长度的, eip+2组合就相当于下一条指令的地址,push eip+2就将它备份入栈,返回后续ret指令恢复eip指令地址而使用的备份数据
jump functions-address
ret 指令
可参考:“The ret instruction pops and jumps to the return address on the stack. A nonzero #n in the RET instruction indicates that after popping the return address, the value #n should be added to the stack pointer.” - MS 文档 或是看我之前翻译
简单的描述 ret
指令,相当远下列过程:
push ebp
mov ebp, esp
... ; // 无局部变量,所以没有申请栈
ret
相当于
push ebp
mov ebp, esp
...
jump return-address
ret 后有数值的
push ebp
mov ebp, esp
sub esp, 0x0ch ; 分配有12个byte的局部变量的空间
...
ret 0x0ch ; 0x0ch == 12
相当于
push ebp
mov ebp, esp
sub esp, 0x0ch ; 分配有12个byte的局部变量的空间
...
add esp, 0x0ch ; 12 个 byte的局部变量的空间清理
jump return-address
有了前面了解:call、ret指令后,就可以了解:调用函数末端-返回-清理栈
调用函数末端-返回-清理栈 _stdcall_stdcall 调用约定中就指令在callee中清理参数压栈的空间
_cdecl _cdecl 调用约定中没有清理
因为_cdecl是在caller中清理参数压栈的空间
在_cdecl 中,push三个int,每个int 占4 个byte,一共12个byte,十进制的12相当于十六进制就是0x0ch,在caller中add esp, 0ch删除参数压栈的空间(所以这样时为何_cdecl可以处理变长参数,因为在caller压栈所少个是知道的,在caller再根据数量清理即可)
References- x86 Disassembly
- 寄存器(cpu工作原理) - 转载于王爽的《汇编语言》PPT
- ARM汇编指令集汇总 - 嵌入式芯片指令(ARM)
- VC6.0和VS2005查看查看C或者C++文件汇编代码的方法
- vs下查看汇编代码