目录
1.练习题回顾
2.反汇编代码
3.分析
lea指令的作用
1.给普通指针赋值
反汇编显示
2.给结构体指针赋值
反汇编显示
mov 指令的作用
1.取普通指针指向地址的值(等价为C语言的*)
反汇编显示
2.取结构体指针指向地址里的值
反汇编显示
3.总结->的作用
4.回到本文分析
细节分析mov ecx,0Ch
解释
对rdi重新恢复为0的解释
1.练习题回顾
求下列代码的执行结果
#include <stdio.h>
char* GetMemory(void)
{char p[] = "hello world";return p;
}void Test(void)
{char* str = NULL;str = GetMemory();printf(str);
}int main()
{Test();return 0;
}
2.反汇编代码
VS2022+x64+debug
#include <stdio.h>
char* GetMemory(void)
{push rbp push rsi push rdi sub rsp,110h lea rbp,[rsp+20h] lea rdi,[rsp+20h] mov ecx,0Ch mov eax,0CCCCCCCCh rep stos dword ptr [rdi] mov rax,qword ptr [__security_cookie (07FF68B18D000h)] xor rax,rbp mov qword ptr [rbp+0E8h],rax lea rcx,[__2D923C74_FileName@c (07FF68B192008h)] call __CheckForDebuggerJustMyCode (07FF68B181375h) nop char p[] = "hello world";lea rax,[p] lea rcx,[string "hello world" (07FF68B18ACA8h)] mov rdi,rax mov rsi,rcx mov ecx,0Ch rep movs byte ptr [rdi],byte ptr [rsi] return p;lea rax,[p]
}mov rdi,rax lea rcx,[rbp-20h] lea rdx,[__xt_z+1E0h (07FF68B18AC80h)] call _RTC_CheckStackVars (07FF68B181311h) mov rax,rdi mov rcx,qword ptr [rbp+0E8h] xor rcx,rbp call __security_check_cookie (07FF68B1811B8h) lea rsp,[rbp+0F0h] pop rdi pop rsi pop rbp ret
............
void Test(void)
{push rbp push rdi sub rsp,108h lea rbp,[rsp+20h] lea rcx,[__2D923C74_FileName@c (07FF68B192008h)] call __CheckForDebuggerJustMyCode (07FF68B181375h) nop char* str = NULL;mov qword ptr [str],0 str = GetMemory();call GetMemory (07FF68B18106Eh) mov qword ptr [str],rax printf(str);mov rcx,qword ptr [str] call printf (07FF68B18119Fh) nop
}lea rsp,[rbp+0E8h] pop rdi pop rbp ret
............
int main()
{push rbp push rdi sub rsp,0E8h lea rbp,[rsp+20h] lea rcx,[__2D923C74_FileName@c (07FF68B192008h)] call __CheckForDebuggerJustMyCode (07FF68B181375h) nop Test();call Test (07FF68B18118Bh) nop return 0;xor eax,eax
}lea rsp,[rbp+0C8h] pop rdi pop rbp ret
3.分析
在深入讲解之前,补充没有讲过的指令:lea,以及lea和mov指令的对比
8086的指令集是这样说的:
lea指令的全称:load effective address,加载有效地址(常用于C语言的&取地址)
mov指令的全称:move
lea指令的作用
1.给普通指针赋值
复制以下代码到VS2022以x86+debug环境调试
int main()
{int a = 0;//映射(映射要加粗)到一个内存地址int* p = &a;return 0;
}
反汇编显示
int a = 0;
mov dword ptr [a],0 int* p = &a;
lea eax,[a]
mov dword ptr [p],eax
备注:称为普通指针的原因是因为和结构体指针做区别
这里的lea eax,[a]
[a]代表a的地址,lea的含义:将a的地址加载到eax寄存器中
2.给结构体指针赋值
复制以下代码到VS2022以x86+debug环境调试
typedef struct INFO
{char c;int id;float f;
}INFO;int main()
{INFO info = { 'A',100,6.6f };INFO* pInfo = &info;return 0;
}
反汇编显示
INFO* pInfo = &info;lea eax,[info] mov dword ptr [pInfo],eax
注意到lea eax,[info],为结构体指针赋值
mov 指令的作用
1.取普通指针指向地址的值(等价为C语言的*)
复制以下下代码到VS2022以x86+debug环境调试
int main()
{int iNUM = 0;int* pNUM = &iNUM;int flag = *pNUM;return 0;
}
反汇编显示
int iNUM = 0;
mov dword ptr [iNUM],0 int* pNUM = &iNUM;
lea eax,[iNUM]
mov dword ptr [pNUM],eax int flag = *pNUM;
mov eax,dword ptr [pNUM]
mov ecx,dword ptr [eax]
mov dword ptr [flag],ecx
对dword ptr [...]的解释:mov dword ptr [iNUM],0 把0赋值到iNUM指向的地址中(这里的[pNUM]为普通指针,dword ptr [iNUM]就是普通指针指向地址的值)
注意到最后三个指令:
mov eax,dword ptr [pNUM]
mov ecx,dword ptr [eax]
mov dword ptr [flag],ecx
这里由ecx做中转寄存器(x86环境不支持mov dword ptr [flag],dword ptr [eax])
2.取结构体指针指向地址里的值
复制以下代码到VS2022以x86+debug环境调试
typedef struct INFO
{char c;int id;float f;
}INFO;int main()
{INFO info = { 'A',100,6.6f };INFO* pInfo = &info;char c = pInfo->c;return 0;
}
反汇编显示
INFO info = { 'A',100,6.6f };
mov byte ptr [info],41h
mov dword ptr [ebp-10h],64h
movss xmm0,dword ptr [__real@40d33333 (0B77BCCh)]
movss dword ptr [ebp-0Ch],xmm0 INFO* pInfo = &info;
lea eax,[info]
mov dword ptr [pInfo],eax char c = pInfo->c;
mov eax,dword ptr [pInfo]
mov cl,byte ptr [eax]
mov byte ptr [c],cl
注意到最后三个指令均为mov指令
->为结构体的特有的符号,虽然不用*表示,但确有*的作用
备注:mov cl,byte ptr [eax] (byte对应cl寄存器)
在结尾处添加以下代码重新调试
char c = pInfo->c; int id = pInfo->id;
对比这两行代码反汇编的不同之处
char c = pInfo->c;
mov eax,dword ptr [pInfo]
mov cl,byte ptr [eax]
mov byte ptr [c],cl int id = pInfo->id;
mov eax,dword ptr [pInfo]
mov ecx,dword ptr [eax+4]
mov dword ptr [id],ecx
注意到:
mov cl,byte ptr [eax+0]
mov ecx,dword ptr [eax+4]
发现eax加的偏移量不同(之前讲过结构体的内存对齐,见63.【C语言】再议结构体(上)文章)
对于char c = pInfo->c;结构体首元素从偏移量为0处开始存储
对于int id = pInfo->id;id的对齐数为4,VS的默认对齐数为8,4<8从偏移量为4的整数倍开始存储
备注:一个空格的存储空间为1个字节
3.总结->的作用
1.取结构体的首地址
2.首地址+成员变量的偏移(例如:eax+?)
3.mov取得到地址里面的值,刚好得到了成员变量的值
4.回到本文分析
由上方lea指令的讲解可知
lea rax,[p]
将p的地址加载到rax中
lea rcx,[string "hello world" (07FF68B18ACA8h)]
将已经写入内存的hello world字符串的首字符的地址加载至rcx中
mov rdi,rax
mov rsi,rcx
rax赋值给rdi,rcx赋值给rsi
mov ecx,0Ch
细节分析mov ecx,0Ch
如果调试的的话,会看到以下奇怪的现象
修改ecx会导致rcx整体都变,ecx为rcx的低32位,那rcx的高32位应该保持不变才对
解释
Intel规定:对ecx的写操作不仅修改了rcx的低32位,还会自动将rcx的高32位清零
详见开发手册,网站:http://x86asm.net/articles/x86-64-tour-of-intel-manuals/
下面截取的为关键信息
-------------------------------------------
在ecx中设置计数次数为0Ch次"hello world\0"一共12个字符(不要忘记隐含的\0)
rep movs byte ptr [rdi],byte ptr [rsi]
[rdi]和[rsi]为指针,从[rsi]处重复复制(rep movs)字节(一次复制一个字节)至[rdi]处
重复复制rcx(0Ch)次,注意:每复制一次,rdi+1,rsi+1,rcx-1
注意:rep指令这里和8086CPU有所不同,VS中默认每复制一次指针+1(运行环境已经提前设置好了),但是8086CPU要设置方向(cld或std)
............
mov rdi,rax
............
pop rdi
虽然恢复了rdi指针的值使其指向了复制过后的字符串的首字符,但是最后出栈的值给了rdi,
rdi重新恢复为0,指针丢失,因此str为野指针
对rdi重新恢复为0的解释
x86+debug环境,VS打开调试模式
有push,就有pop
;保存rbp,rsi,rdi的值
push rbp
push rsi
push rdi
-------------------------------------------------------------------------------------------
;恢复rbp,rsi,rdi原来的值
pop rdi
pop rsi
pop rbp
执行push rbp之前,查看寄存器
注意到rdi=0,因此在GetMemroy函数的最后pop rdi时,rdi的值恢复为0