也问关于this指针

ancient 2006-02-06 10:55:42
粗看了前篇"关于this指针的深入探讨"关于this的讨论,我的问题可能更简单和"工程化"一点
题目有点不准确,我想在大多数情况下,把"this 指针"叫作"this 参数"可能更合适一点

问题:
1.this作为类方法的一个隐含参数,是如何入栈的(比如是作为最左的参数还是最右的)或是存放在固定的寄存器中
2.问题1跟调用惯例有关吗?

例如:
1.我希望windows的回调函数不是一个普通的函数而是一个同样原型的类方法,如何写一个调用通道把对象的地址作为this参数插进去(如果不是windows而是其它gui呢?)
2.假定PMethod是一个指向某Class1::Method(int val)方法的指针,class1是一个指向Class1对象的指针,如何通过class1和PMethod来达到调用class1->Method(anyInt)的目的(注意:调用者可能不知道Method的名子,而只有PMethod指针),是不是用类似于{push class1; PMethod(anyInt);}的代码就可以做到?
...全文
220 9 打赏 收藏 转发到动态 举报
写回复
用AI写文章
9 条回复
切换为时间正序
请发表友善的回复…
发表回复
StarGate1 2006-02-06
  • 打赏
  • 举报
回复
看着就头晕
lovedna 2006-02-06
  • 打赏
  • 举报
回复
SG1
ancient 2006-02-06
  • 打赏
  • 举报
回复
汇编的代码看了,我想知道的是这种调用的方法跟os和compiler以及调用惯例有没有关系,3x
laolaoliu2002 2006-02-06
  • 打赏
  • 举报
回复
#include <stdio.h>
#include <string.h>

class BaseClass
{
  private:
    char Buffer[32];
  public:
    void SetBuffer(char *String)
    {
      strcpy(Buffer,String); // 存在缓冲区溢出漏洞
    }
    virtual void PrintBuffer()
    {
      printf(%s\n,Buffer);
    }
};

class MyClass1:public BaseClass
{
  public:
    void PrintBuffer()
    {
      printf(MyClass1: );
      BaseClass::PrintBuffer();
    }
};

class MyClass2:public BaseClass
{
  public:
    void PrintBuffer()
    {
      printf(MyClass2: );
      BaseClass::PrintBuffer();
    }
};

void main()
{
  BaseClass *Object[2];

  Object[0] = new MyClass1;
  Object[1] = new MyClass2;

  Object[0]->SetBuffer(string1);
  Object[1]->SetBuffer(string2);
  Object[0]->PrintBuffer();
  Object[1]->PrintBuffer();
}

  以下是bo2.cpp编译后的运行结果:

[backend@isbase test]> ./bo2
MyClass1: string1
MyClass2: string2
[backend@isbase test]>
laolaoliu2002 2006-02-06
  • 打赏
  • 举报
回复
构建堆栈。为Object[]数组保留8个字节(即两个4字节指针地址),则Object[0]的指针存放在0xfffffff8(%ebp),Object[1]的指针存放在0fffffffc(%ebp)。接着保存寄存器。

0x8049409 <main+9>:   push  $0x24
0x804940b <main+11>:  call  0x804b580 <__builtin_new>
0x8049410 <main+16>:  add  $0x4,%esp

  首先调用__builtin_new,在堆(heap)中分配0x24(36字节)给Object[0],并将其首地址保存到EAX寄存器中。这36字节中前32字节是Buffer变量的,后4字节由VPTR占用。

0x8049413 <main+19>:  mov  %eax,%eax
0x8049415 <main+21>:  mov  %eax,%ebx
0x8049417 <main+23>:  push  %ebx
0x8049418 <main+24>:  call  0x804c90c <__8MyClass1>
0x804941d <main+29>:  add  $0x4,%esp

  将对象的首地址压栈,然后调用__8MyClass1函数。这其实是MyClass1对象的构造函数(constructor)。

(gdb) disassemble __8MyClass1
Dump of assembler code for function __8MyClass1:
0x804c90c <__8MyClass1>:    push  %ebp
0x804c90d <__8MyClass1+1>:   mov  %esp,%ebp
0x804c90f <__8MyClass1+3>:   push  %ebx
0x804c910 <__8MyClass1+4>:   mov  0x8(%ebp),%ebx

  寄存器EBX现在存放着指向分配的36个字节的指针(在C++语言中,称之为This指针)。

0x804c913 <__8MyClass1+7>:   push  %ebx
0x804c914 <__8MyClass1+8>:   call  0x804c958 <__9BaseClass>
0x804c919 <__8MyClass1+13>:   add  $0x4,%esp

  首先调用基类BaseClass的构造函数。

(gdb) disassemble __9BaseClass
Dump of assembler code for function __9BaseClass:
0x804c958 <__9BaseClass>:    push  %ebp
0x804c959 <__9BaseClass+1>:   mov  %esp,%ebp
0x804c95b <__9BaseClass+3>:   mov  0x8(%ebp),%edx

  寄存器EDX现在存放着指向分配的36个字节的指针(This指针)。

0x804c95e <__9BaseClass+6>:   movl  $0x804e01c,0x20(%edx)

  将0x804e01c存放到EDX+0x20(=EDX+32)。让我们看看该0x804e01c地址内存数据:

(gdb) x 0x804e01c
0x804e01c <__vt_9BaseClass>:  0x00000000

  可以看到这个存放到EDX+0x20(即该对象的VPTR位置)的地址是基类BaseClass的VTABLE地址。
  现在回到MyClass1对象的构造函数:

0x804c91c <__8MyClass1+16>:   movl  $0x804e010,0x20(%ebx)

  将0x804e010存放到EBX+0x20(即VPTR)。同样让我们看看该0x804e010地址内存数据:

(gdb) x 0x804e010
0x804e010 <__vt_8MyClass1>:   0x00000000

  现在,我们知道VPTR被改写了,再在它的内容是MyClass1对象的VTABLE地址。当返回到main()函数时寄存器EAX中存放着该对象在内存中的指针。

0x8049420 <main+32>:  mov  %eax,%esi
0x8049422 <main+34>:  jmp  0x8049430 <main+48>
0x8049424 <main+36>:  call  0x8049c3c <__throw>
0x8049429 <main+41>:  lea  0x0(%esi,1),%esi
0x8049430 <main+48>:  mov  %esi,0xfffffff8(%ebp)

  将得到的地址指针赋予Object[0]。然后程序对Object[1]进行同样的处理,只不过返回的地址不同罢了。在经过以上对象初始化处理后,将执行以下指令:

0x8049458 <main+88>:  push  $0x804cda2
0x804945d <main+93>:  mov  0xfffffff8(%ebp),%eax
0x8049460 <main+96>:  push  %eax

  将0x804cda2和Object[0]的值压栈。观察一下0x804cda2的内容:

(gdb) x/s 0x804cda2
0x804cda2 <_IO_stdin_used+30>:  string1

  可知该地址存放了将要通过基类BaseClass的SetBuffer函数拷贝到Buffer中的字符串string1。

0x8049461 <main+97>:  call  0x804c930 <SetBuffer__9BaseClassPc>
0x8049466 <main+102>:  add  $0x8,%esp

  调用基类BaseClass的SetBuffer()方法。注意到这种SetBuffer方法的调用是“静态绑定”(因为它不是虚拟方法)。对Object[1]的处理也是一样的。

  为了验证这两个对象在运行时都被正确地初始化,我们将要设置如下断点:

0x8049410: 获得第一个对象的地址。
0x804943a: 获得第二个对象的地址。
0x804947a: 检验对象的初始化是否正确。

(gdb) break *0x8049410
Breakpoint 1 at 0x8049410
(gdb) break *0x804943a
Breakpoint 2 at 0x804943a
(gdb) break *0x804947a
Breakpoint 3 at 0x804947a
laolaoliu2002 2006-02-06
  • 打赏
  • 举报
回复
编译器在编译时首先检查BaseClass基类的声明。在本例,编译器首先为私有变量Buffer(字符串型)保留32个字节,接着为非虚拟方法SetBuffer()计算并指定相应的调用地址(静态绑定处理),最后在检查到虚拟方法PrintBuffer()时,将做动态绑定处理,即在类中分配4个字节用以存放该虚拟方法的指针。结构如下:

    BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBVVVV

说明: B 变量Buffer占用。
    V 虚拟方法指针占用。

  这个指针通常被称为“VPTR”(Virtual Pointer),它指向一个“VTABLE”结构中的函数入口之一。每一个类都有一个VTABLE。如下图所示:

Object[0]: BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBVVVV
                      =+==
            |
     +------------------------------+
     |
     +--> VTABLE_MyClass1: IIIIIIIIIIIIPPPP

Object[1]: BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBWWWW
                      =+==
            |
     +------------------------------+
     |
     +--> VTABLE_MyClass2: IIIIIIIIIIIIQQQQ

说明: B 变量Buffer占用。
    V 指向VTABLE_MyClass1的VPTR指针占用。
    W 指向VTABLE_MyClass2的VPTR指针占用。
    I 其它用途的数据
    P MyClass1对象实例的PrintBuffer()方法的地址指针。
    Q MyClass2对象实例的PrintBuffer()方法的地址指针。

  我们可以发现,VPTR位于进程内存中Buffer变量之后。即当调用危险的strcpy()函数时有可能覆盖VPTR的内容!
  
  根据rix的研究测试,对于Windows平台上的Visual C++ 6.0,VPTR位于对象的起始位置,因此这里提到的技术无法产生作用。这点与GNU C++有很大的不同。


---[[ 剖析VPTR ]]--------------------------------------

  在Linux下当然是使用GDB来分析了:

[backend@isbase test]> gcc -o bo2 bo2.cpp
[backend@isbase test]> gdb bo2
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type show copying to see the conditions.
There is absolutely no warranty for GDB. Type show warranty for details.
This GDB was configured as i386-redhat-linux...
(gdb) disassemble main
Dump of assembler code for function main:
0x8049400 <main>:    push  %ebp
0x8049401 <main+1>:   mov  %esp,%ebp
0x8049403 <main+3>:   sub  $0x8,%esp
0x8049406 <main+6>:   push  %edi
0x8049407 <main+7>:   push  %esi
0x8049408 <main+8>:   push  %ebx
0x8049409 <main+9>:   push  $0x24
0x804940b <main+11>:  call  0x804b580 <__builtin_new>
0x8049410 <main+16>:  add  $0x4,%esp
0x8049413 <main+19>:  mov  %eax,%eax
0x8049415 <main+21>:  mov  %eax,%ebx
0x8049417 <main+23>:  push  %ebx
0x8049418 <main+24>:  call  0x804c90c <__8MyClass1>
0x804941d <main+29>:  add  $0x4,%esp
0x8049420 <main+32>:  mov  %eax,%esi
0x8049422 <main+34>:  jmp  0x8049430 <main+48>
0x8049424 <main+36>:  call  0x8049c3c <__throw>
0x8049429 <main+41>:  lea  0x0(%esi,1),%esi
0x8049430 <main+48>:  mov  %esi,0xfffffff8(%ebp)
0x8049433 <main+51>:  push  $0x24
0x8049435 <main+53>:  call  0x804b580 <__builtin_new>
0x804943a <main+58>:  add  $0x4,%esp
0x804943d <main+61>:  mov  %eax,%eax
0x804943f <main+63>:  mov  %eax,%esi
0x8049441 <main+65>:  push  %esi
0x8049442 <main+66>:  call  0x804c8ec <__8MyClass2>
0x8049447 <main+71>:  add  $0x4,%esp
0x804944a <main+74>:  mov  %eax,%edi
0x804944c <main+76>:  jmp  0x8049455 <main+85>
0x804944e <main+78>:  mov  %esi,%esi
0x8049450 <main+80>:  call  0x8049c3c <__throw>
0x8049455 <main+85>:  mov  %edi,0xfffffffc(%ebp)
0x8049458 <main+88>:  push  $0x804cda2
0x804945d <main+93>:  mov  0xfffffff8(%ebp),%eax
0x8049460 <main+96>:  push  %eax
0x8049461 <main+97>:  call  0x804c930 <SetBuffer__9BaseClassPc>
0x8049466 <main+102>:  add  $0x8,%esp
0x8049469 <main+105>:  push  $0x804cdaa
---Type <return> to continue, or q <return> to quit---
0x804946e <main+110>:  mov  0xfffffffc(%ebp),%eax
0x8049471 <main+113>:  push  %eax
0x8049472 <main+114>:  call  0x804c930 <SetBuffer__9BaseClassPc>
0x8049477 <main+119>:  add  $0x8,%esp
0x804947a <main+122>:  mov  0xfffffff8(%ebp),%edx
0x804947d <main+125>:  mov  0x20(%edx),%eax
0x8049480 <main+128>:  add  $0x8,%eax
0x8049483 <main+131>:  mov  0xfffffff8(%ebp),%edx
0x8049486 <main+134>:  push  %edx
0x8049487 <main+135>:  mov  (%eax),%edi
0x8049489 <main+137>:  call  *%edi
0x804948b <main+139>:  add  $0x4,%esp
0x804948e <main+142>:  mov  0xfffffffc(%ebp),%edx
0x8049491 <main+145>:  mov  0x20(%edx),%eax
0x8049494 <main+148>:  add  $0x8,%eax
0x8049497 <main+151>:  mov  0xfffffffc(%ebp),%edx
0x804949a <main+154>:  push  %edx
0x804949b <main+155>:  mov  (%eax),%edi
0x804949d <main+157>:  call  *%edi
0x804949f <main+159>:  add  $0x4,%esp
0x80494a2 <main+162>:  xor  %eax,%eax
0x80494a4 <main+164>:  jmp  0x80494d0 <main+208>
0x80494a6 <main+166>:  jmp  0x80494d0 <main+208>
0x80494a8 <main+168>:  push  %ebx
0x80494a9 <main+169>:  call  0x804b4f0 <__builtin_delete>
0x80494ae <main+174>:  add  $0x4,%esp
0x80494b1 <main+177>:  jmp  0x8049424 <main+36>
0x80494b6 <main+182>:  push  %esi
0x80494b7 <main+183>:  call  0x804b4f0 <__builtin_delete>
0x80494bc <main+188>:  add  $0x4,%esp
0x80494bf <main+191>:  jmp  0x8049450 <main+80>
0x80494c1 <main+193>:  jmp  0x80494c8 <main+200>
0x80494c3 <main+195>:  call  0x8049c3c <__throw>
0x80494c8 <main+200>:  call  0x8049fc0 <terminate__Fv>
0x80494cd <main+205>:  lea  0x0(%esi),%esi
0x80494d0 <main+208>:  lea  0xffffffec(%ebp),%esp
0x80494d3 <main+211>:  pop  %ebx
0x80494d4 <main+212>:  pop  %esi
0x80494d5 <main+213>:  pop  %edi
---Type <return> to continue, or q <return> to quit---
0x80494d6 <main+214>:  leave 
0x80494d7 <main+215>:  ret  
0x80494d8 <main+216>:  nop  
0x80494d9 <main+217>:  nop  
0x80494da <main+218>:  nop  
0x80494db <main+219>:  nop  
0x80494dc <main+220>:  nop  
0x80494dd <main+221>:  nop  
0x80494de <main+222>:  nop  
0x80494df <main+223>:  nop  
End of assembler dump.
(gdb)

  以下是对该程序汇编代码的解释:

0x8049400 <main>:    push  %ebp
0x8049401 <main+1>:   mov  %esp,%ebp
0x8049403 <main+3>:   sub  $0x8,%esp
0x8049406 <main+6>:   push  %edi
0x8049407 <main+7>:   push  %esi
0x8049408 <main+8>:   push  %ebx
ancient 2006-02-06
  • 打赏
  • 举报
回复
不过我觉得最终的目标代码形式应该跟编译原理关系不大,
源代码和目标代码都有各自的标准,而编译技术主要是研究翻译的方法
ancient 2006-02-06
  • 打赏
  • 举报
回复
hehe,替二楼的说一下,他贴的东西里面有构造函数的调用方法,算是有关吧
0x8049413 <main+19>:  mov  %eax,%eax
0x8049415 <main+21>:  mov  %eax,%ebx
0x8049417 <main+23>:  push  %ebx
0x8049418 <main+24>:  call  0x804c90c <__8MyClass1>
0x804941d <main+29>:  add  $0x4,%esp
逸学堂 2006-02-06
  • 打赏
  • 举报
回复
laolaoliu2002
这个和this没有什么联系啊!
主要是将内存溢出的啊?

至于this,如果深入编译内部,说简单也可,说复杂也对.
至于楼主的第一问题
1.this作为类方法的一个隐含参数,是如何入栈的(比如是作为最左的参数还是最右的)或是存放在固定的寄存器中
在debug下,
class A a;
a.fun();
汇编调用如下
push ecx &a;// 记录a的this值
push .....// 偏移地址
关于this指针的深入探讨http://community.csdn.net/Expert/TopicView3.asp?id=4532843
这篇文章仔细看看会有帮助的.
我现在已经不敢对this妄言了.深入一个程序内部,了解机理,必须学好汇编
熟悉编译原理!

64,648

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧