汇编与C/C++的故事

  其实我知道这个标题实在是太广泛了,但是下面所有东西都是和汇编,C++有关的。

  1. stdcall与cdecl
      首先他们共属于函数约定(calling convention),详情参考wiki(https://en.wikipedia.org/wiki/Calling_convention).总结如下:
      a. 两者都是参数从右至左进栈.
      b. stdcall是winapi标准默认调用方式(也就是WINAPI这个宏),cdecl是默认C调用方式.
      c. stdcall在函数返回前清栈(函数自己清栈),cdecl在函数返回后清栈(由调用者清栈).所以可变参数列表只支持cdecl调用方式.
      以上是瞎bb的理论阶段,那么上面所说的清栈到底是个什么东西呢?我从C代码层面明明没有感觉到区别啊!
      下面用vs2015汇编最简单的函数比较一下(精华在注释里面):
void __stdcall func(int) {}
/*
01201690  push        ebp
01201691  mov         ebp,esp
01201693  sub         esp,0C0h
01201699  push        ebx
0120169A  push        esi
0120169B  push        edi
0120169C  lea         edi,[ebp-0C0h]
012016A2  mov         ecx,30h
012016A7  mov         eax,0CCCCCCCCh
012016AC  rep stos    dword ptr es:[edi]
012016AE  pop         edi
012016AF  pop         esi
012016B0  pop         ebx
012016B1  mov         esp,ebp
012016B3  pop         ebp
// 上面代码都可忽略,除了push,pop没做什么。
// 注意这里在函数返回前用了栈平衡操作
// ret函数是为了平衡下面012016E0  call        func(0120123Fh)
// 4 才是stdcall的精华,函数返回前平衡了012016DE  push        1导致的栈变化。
012016B4  ret         4
*/

int main()
{
    func(1);
    //012016DE  push        1
    //012016E0  call        func(0120123Fh)
    // call函数后面没有任何对ESP操作的代码,因为在函数ret之前就完成了清栈工作,这就是stdcall.
    return 0;
}

同样的我们比较一下把stdcall改为cdecl,看下汇编代码:

void __cdecl func(int) {}
/*
00B71690  push        ebp
00B71691  mov         ebp,esp
00B71693  sub         esp,0C0h
00B71699  push        ebx
00B7169A  push        esi
00B7169B  push        edi
00B7169C  lea         edi,[ebp-0C0h]
00B716A2  mov         ecx,30h
00B716A7  mov         eax,0CCCCCCCCh
00B716AC  rep stos    dword ptr es:[edi]
00B716AE  pop         edi
00B716AF  pop         esi
00B716B0  pop         ebx
00B716B1  mov         esp,ebp
00B716B3  pop         ebp
// 同样注意这里,当改为cdecl调用后,ret指令后面就没东西了
00B716B4  ret
*/

int main()
{
    func(1);
    /*
    00B716DE  push        1
    00B716E0  call        func (0B71343h)
    // 函数调用结束后,生成的汇编代码函数ret之后,改变了ESP用以平衡之前的push 1操作
    00B716E5  add         esp,4
    */
    return 0;
}

2 . C++的返回值在汇编的eax寄存器位置
  void返回值类型的函数并不意味着一定不能返回参数.

void __stdcall func1() 
{
    __asm
    {
        mov eax, 10
    }
}

int __stdcall func2()
{
    __asm
    {
        mov eax, 11
    }
}
int main()
{
    int x=0;
    __asm
    {
        call func1
        mov x,eax
    }
    printf_s("x=%d  func2:%d\n", x,func2());
    return 0;
}

  上面两段函数,内部代码是一样的,虽然函数返回值定义一个是void类型,一个是int类型,但都可以传递值出来。因为C/C++的返回值存在eax寄存器上。

  3. 引用与指针
  众所周知,引用是变量的别名,那么从汇编层面来看,引用时如何处理的呢?

void __stdcall f(int a,int b,int &c)
{
    // c = a+b;
    __asm
    {
        mov eax, dword ptr[a]
        add eax, dword ptr[b]
        mov ecx, dword ptr[c]  // 取得c的地址,注意这里用的是mov,不是lea
        mov dword ptr [ecx],eax  
    }
}

int main()
{
    int c=0;
    // f(1,2,c);
    __asm
    {
        // 从右至左压栈,所以进栈顺序时c的地址,2,1
        // 最后通过call进入f函数
        lea  eax, [c]
        push eax
        push 2
        push 1
        call f
    }
    printf_s("%d", c);
    return 0;
}

  有趣的是,将上述代码中f函数参数类型改为int*代码仍然是可以正确运行的。所以从汇编层面看,引用传递与指针相差无几,都是传递变量地址。从C语言代码层面看,引用传递时的所有操作之前地址都会被解引用。比如上面的c=a+b。所以这也正是为什么引用的变量不能被赋为空指针的原因。
  
  4. 汇编调用printf
  因为printf允许接受可变参数,所以肯定是cdecl调用方式。其实还是亘古不变的老道理,参数由右至左压栈,然后call函数,返回值保存在eax中。

    const char* printf_str = "name:%s age:%d\n";
    char name[] = "zhou";
    int result = -1;
    __asm
    {
        push 20   // age压栈
        lea eax,dword ptr[name]
        push eax  // name 压栈
        mov eax,dword ptr[printf_str]
        push eax  // 打印格式字符串压栈
        call printf
        add esp,12  // cdecl自己清栈
        mov result,eax // printf返回值
    }
    printf_s("last printf result:%d\n", result);

  其中有两点需要特殊说明:
  a. 同样的字符串操作,char*压栈时直接push变量本身就好了(因为指针保存的就是字符串的地址),在对char数组压栈时,需要先lea获取首字母地址再push。
  b. printf其实是有返回值的,返回输出字符的个数,若出错,则返回负数。
  

  5. c++调用实例方法
  首先先区分函数(function)和方法(method)的区别: in-c-what-is-the-difference-between-a-method-and-a-function
  那么method相比于function会隐性传入一个对象指针作为this参数,那么从汇编层次可以更清晰的看到这种传参过程。(精华在注释)

class A
{
public:
    int f(int x)
    {
        // 此处省略部分汇编代码
        // 从ecx取出对象地址赋值给this指针
        //00221850  mov         dword ptr[this], ecx
        return x;
        //00221853  mov         eax, dword ptr[x]
        //00221857  pop         esi
        //00221858  pop         ebx
        //00221859  mov         esp, ebp
        //0022185B  pop         ebp
        // 看起来采用stdcall的调用方式,函数内部返回前平衡栈。
        //0022185C  ret         4
    }
};

int main()
{
    A* a = new A();
    a->f(10);
    //002218C8  push        0Ah
    //002218CA  mov         ecx, dword ptr[a] // 在正式调用实例方法之前将a对象地址放入了ecx中
    //002218CD  call        A::f(02213E8h)
    return 0;
}

  总结:
  1. 在调用实例方法之前将实例对象地址放入ecx寄存器并在函数开始将其取出赋值给this对象,这就是c++在调用method时this对象的隐性传入方式。
  在实例对象返回前完成了栈平衡操作,所以是calling convention是stdcall。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值