对于汇编语言,个人觉得更多的需要的是耐心,不像高级语言那样有繁杂的语法体系。
在参数传递方式和strcpy为何不安全中讲解了函数调用过程以及参数传递的方式,本文从汇编层面说一下Windows下x64的参数调用相应的规则。通过汇编能让你更彻底的了解到底什么是指针,参数如何传递的。
Microsoft x64 调用约定
适用范围:主要用于 Windows 操作系统上的 64 位程序。
参数传递规则:前四个整数类型(包括指针)或浮点类型的参数会依次通过寄存器 rcx、rdx、r8 和 r9 传递,后续的参数则通过栈来传递。例如下面的 C++ 函数调用:
#include <iostream>
int test_add(int a, int b, int c, int d, int e, int f) {
std::cout << a + b + c + d + e + f << std::endl;
return 0;
}
int main()
{
test_add(1, 2, 3, 4, 5, 6);
std::cout << "Hello World!\n";
}
main函数对应的汇编代码:
int main()
{
00007FF6C5702430 push rbp
00007FF6C5702432 push rdi
00007FF6C5702433 sub rsp,0F8h
00007FF6C570243A lea rbp,[rsp+30h]
00007FF6C570243F lea rcx,[__29F341FE_ConsoleApplication1@cpp (07FF6C5713068h)]
00007FF6C5702446 call __CheckForDebuggerJustMyCode (07FF6C5701401h)
00007FF6C570244B nop
test_add(1, 2, 3, 4, 5, 6);
00007FF6C570244C mov dword ptr [rsp+28h],6
00007FF6C5702454 mov dword ptr [rsp+20h],5
00007FF6C570245C mov r9d,4
00007FF6C5702462 mov r8d,3
00007FF6C5702468 mov edx,2
00007FF6C570246D mov ecx,1
00007FF6C5702472 call test_add (07FF6C57012BCh)
00007FF6C5702477 nop
std::cout << "Hello World!\n";
00007FF6C5702478 lea rdx,[string "Hello World!\n" (07FF6C570AC28h)]
00007FF6C570247F mov rcx,qword ptr [__imp_std::cout (07FF6C5711190h)]
00007FF6C5702486 call std::operator<<<std::char_traits<char> > (07FF6C570108Ch)
00007FF6C570248B nop
}
注意其中对于test_add的调用,是从右到左依次入栈的。从汇编代码看与上面所说的规则是一致的。相应的栈内存如下(具体可参见Visual Studio快捷键介绍和高级玩法)

00007FF6C5702472 call test_add (07FF6C57012BCh)看看这句话做了什么:
调用之前寄存器的值:
RAX = 0000000000000001 RBX = 0000000000000000 RCX = 0000000000000001 RDX = 0000000000000002 RSI = 0000000000000000 RDI = 0000000000000000 R8 = 0000000000000003 R9 = 0000000000000004 R10 = 0000000000000012 R11 = 000000326D4FFD00 R12 = 0000000000000000 R13 = 0000000000000000 R14 = 0000000000000000 R15 = 0000000000000000 RIP = 00007FF6C5702472 RSP = 000000326D4FFC50 RBP = 000000326D4FFC80 EFL = 00000206
进入到test_add后寄存器的值:
RAX = 0000000000000001 RBX = 0000000000000000 RCX = 0000000000000001 RDX = 0000000000000002 RSI = 0000000000000000 RDI = 0000000000000000 R8 = 0000000000000003 R9 = 0000000000000004 R10 = 0000000000000012 R11 = 000000326D4FFD00 R12 = 0000000000000000 R13 = 0000000000000000 R14 = 0000000000000000 R15 = 0000000000000000 RIP = 00007FF6C5702370 RSP = 000000326D4FFC48 RBP = 000000326D4FFC80 EFL = 00000206
对比可知rsp和rip发生了变化,查看此时堆栈的值:

这个值恰恰是call指令后面的地址。从这里可以看出call做了什么:
将call后面的指令压入堆栈中;rsp = rsp -8(栈是从高地址向低地址增长的)
跳转到call 函数所在位置 jump to fun_address rip指向新的地址
栈是1字节存储的,地址8字节需要-8,这也就解释了rsp为什么要-8。(000000326D4FFC50 - 000000326D4FFC48 = 8(十六进制))
注意: 00007FF6C5702430 push rbp 会将rsp-8
windbg源代码调试
设置源代码就能进行源码级别的调试了。VS本身就能调试,搞这么费劲干嘛那?当需要跨语言开发的时候,比如通过Java或者Python和C++协同开发的时候,通过attach进程,然后就能够调试C++代码了。 还有exe出现crash的时候,windbg就可以排上用场了。经典的蓝屏也可以通过windbg来分析。

只要有pdb,在另一台电脑上,有源代码,指定之后也可以进行调试。
原理:在 Visual Studio 中,你可以手动指定源代码文件的新路径,让调试器能够找到正确的源代码。 操作步骤
打开调试会话:在另一台电脑上打开需要调试的程序,并启动调试会话。 指定源代码路径:当调试器提示找不到源代码文件时,你可以在 “源文件”
窗口中右键单击,选择 “浏览”,然后手动指定源代码文件的新路径。这样,调试器就会使用新的路径来查找源代码文件。
查看调用堆栈

这里传递的是引用, 查看Args to Child(子程序)的参数共四个,按显示的是按照参数从左到右的顺序,这点很重要。
通过dd命令查看。这说明引用的本质还是指针。

这一次参数使用指针,我们发现两者的值是一样的。这也说明了引用的本质是指针。

查看汇编代码
我想查看调用这个函数的函数汇编代码,使用u命令即可查查看汇编代码。通过调用堆栈能够看到RetAddr或者Call site,基于此,我们就可以查看相应的汇编代码了。

通过RetAddr或者Call Site的地址,在这里两者的值是一样的。

这个是有跳转的

调用堆栈如下,上图对应的是下图中的最前面两个调用堆栈:


更多内容,欢迎关注我的微信公众号:半夏之夜的无情剑客。

4925

被折叠的 条评论
为什么被折叠?



