When the object layout is known, the address is adjusted when the C ++ Object Pointer is converted, and the layout pointer

Source: Internet
Author: User
Tags sub command

When the object layout is known, the address is adjusted when the C ++ Object Pointer is converted, and the layout pointer

I noticed this problem when debugging and researching the netscape browser plug-in development. That is, when the object layout is known (that is, there is an inheritance relationship between objects), pointers of different types of objects are converted (whether implicit conversion from bottom to top or forced conversion from top to bottom) the compiler will adjust the corresponding pointer value according to the object layout. Both the microsoft compiler and the gcc compiler perform this action because it is related to the C ++ object model.

 

The following code is a simple example:

 

#include <stdio.h>class A{public:    int x;    void foo1() { printf("A:foo1 \n"); };};class B : public A{public:    double y;    virtual void foo2() { printf("B:foo2 \n"); };};int _tmain(int argc, _TCHAR* argv[]){    B* pb = (B*)0x00480010;    A* pa = pb;    printf(" pb:%p\n pa:%p\n", pb, pa);    getchar();    return 0;}

 

The code above shows that B inherits from A, A does not have virtual functions, and B has virtual functions. Therefore, the starting position of object A does not contain the virtual function table pointer. The starting position of object B contains the virtual function table pointer. In VC 2005, the following output is displayed:

 

Petabyte: 00480010
CA: 00480018

The difference between the two addresses is 8 bytes. The addresses of the two objects are not equal because of the relationship between the virtual function table pointers. The pointer of the virtual function table usually occupies 4 Bytes. In the output result, the difference value is related to the object layout, that is, it is also related to the object alignment settings in the compiler options. However, the two addresses have a difference value determined during compilation. The difference value may be 4 bytes under different conditions. For example, if the member y of object B is changed to the int type. The difference value is 4 bytes.

 

In the preceding demo, the pointer type is implicitly converted from B * To A *, and the address value is increased by 8 Bytes. If the pointer type is forcibly converted from A * to B *, The address will be adjusted in the opposite way. Observe the assembly code and you can see that the offset adjustment of this address value is the operation that the compiler inserts during compilation, completed by the ADD/SUB command. The assembly code is no longer displayed here.

 

It is worth mentioning that in C ++, struct and class are essentially no different, except that the default access levels of members are different. Therefore, when declaring an object in the code above, using the class or struct keyword does not affect the conclusion.

 

The example above briefly illustrates that when an object has an inheritance relationship, the address value may be adjusted during pointer conversion, which is completed by the compiler. In the preceding example, the address difference between objects is caused by whether the object header contains a virtual function table pointer. The following is a more detailed example to illustrate this problem. That is, address adjustment when an object instance contains multiple child objects (with multiple parent classes. And why, in this case, the object's destructor must be a virtual function.

 

The code for the second example is as follows:

 

  

#include <string.h>#include <stdio.h>//Parent 1class P1{public:    int m_x1;    int m_x2;    int m_x3;public:    P1()    {        m_x1 = 0x12345678;        m_x2 = 0xAABBCCDD;        m_x3 = 0xEEFF0011;        printf("P1 constructor.\n");    }    virtual ~P1()    {        printf("P1 destructor.\n");    }    virtual void SayHi()    {        printf("P1: hello!\n");    }};//Parent 2: 16 Bytesclass P2{public:    char m_name[12];public:    P2()    {        strcpy(m_name, "Jack");        printf("P2 constructor.\n");    }    virtual ~P2()    {        printf("P2 destructor.\n");    }    virtual void ShowName()    {        printf("P2 name: %s\n", m_name);    }};//Parent 3: 16 Bytesclass P3{public:    char m_nick[12];public:    P3()    {        strcpy(m_nick, "fafa");        printf("P3 constructor.\n");    }    virtual ~P3()    {        printf("P3 destructor.\n");    }    virtual void ShowNick()    {        printf("P3 Nick: %s\n", m_nick);    }};//Child1class C1 : public P1, public P2, public P3{public:    int m_y1;    int m_y2;    int m_y3;    int m_y4;public:    C1()    {        m_y1 = 0x01;        m_y2 = 0x02;        m_y3 = 0x03;        m_y4 = 0x04;        printf("C1 constructor.\n");    }    virtual ~C1()    {        printf("C1 destructor.\n");    }    virtual void SayHi()    {        printf("C1: SayHi\n");    }    virtual void C1_Func_01()    {        printf("C1: C1_Func_01\n");    }};int main(int argc, char* argv[]){    C1 *c1 = new C1();    P1 *p1 = c1;    P2 *p2 = c1;    P3 *p3 = c1;    p1->SayHi();    printf("c1: %p\np1: %p\np2: %p\np3: %p\n", c1, p1, p2, p3);    //show object's binary data    unsigned char* pBytes = (unsigned char*)(c1);    //_CrtMemBlockHeader *pHead = pHdr(pBytes);    size_t cb = sizeof(C1);    unsigned int i;    for(i = 0; i < cb; i++)    {        printf("%02X ", pBytes[i] & 0xFF);        if((i & 0xF) == 0xF)            printf("\n");    }    printf("\n");    //_CrtDumpMemoryLeaks();    delete p2;    return 0;}

 

The main content of the second example is: subclass C1, which has three parent classes: P1, P2, and P3. All classes have virtual destructor, that is, the object instance has a virtual function table pointer. It shows the inheritance relationship of the class:

 

  

  

Figure 1. class inheritance

 

When Class C1 is constructed, it contains three child objects: P1, P2, and P3. We know that the first virtual function table pointer of the parent class P1 adopts the virtual function table pointer of C1, that is, the subclass has the ability to overwrite the parent class virtual function, this is an important part of implementing polymorphism in C ++. Therefore, in the C1 object instance, there is actually no virtual function table pointer of P1. Instead, the subclass is directly used. So P2 and P3 are the parent classes of C1. How can we obtain the virtual function table content of P2 and P3? This involves the C ++ object model.

 

The virtual function table of P2 and P3 cannot be merged with the content of the virtual function table of C1, which makes it difficult for the compiler to call virtual functions of P2 and P3. Instead, it is offset backward, that is, except for the first parent class, other parent classes must each retain an independent virtual function table pointer in the object. That is, the object has an independent perspective of P2 and P3. In this example, the object has a total of three virtual function table pointers, three perspectives: P1/C1, P2, P3. Shows the object model:

 

  

Figure 2. Object Model with multiple child objects

 

The object model of the C1 instance is provided. When we convert the pointer to C1 to the pointer to P2 or P3, we have already said that the compiler has inserted an adjustment to the address value. In this example, I set the size of the space occupied by the member variables so that the address offset values are 0x10, 0x20, respectively. The output of the above Code is as follows (the results obtained by using VC compilation in Windows or g ++ compilation in Linux are similar, but only the address values of objects are dynamically allocated are different ):

 

P1 constructor.P2 constructor.P3 constructor.C1 constructor.C1: SayHic1: 003E5068p1: 003E5068p2: 003E5078p3: 003E5088B8 76 41 00 78 56 34 12 DD CC BB AA 11 00 FF EEA8 76 41 00 4A 61 63 6B 00 CD CD CD CD CD CD CD98 76 41 00 66 61 66 61 00 CD CD CD CD CD CD CD01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00C1 destructor.P3 destructor.P2 destructor.P1 destructor.

 

In the middle of the output, the binary content of the object is provided, which is dumped. You can see the first behavior from the P1/C1 perspective. The second action is P2 and the third action is P3. The fourth behavior is the member variable of C1.

 

At the same time, we can see that when we call delete on the P2 * pointer, the object can be correctly parsed. This is because when the compiler constructs the C1 object, because P2 and P3 destructor are virtual functions, the compiler also adds address adjustment to the destructor. Since the compiler knows the layout of P2 and P3 relative to C1, it knows the real memory starting point of the object. Therefore, it inserts the corresponding trunk code in the code segment, that is, after the object address is subtracted from the offset value, get the actual address of the object and jump to the C1 destructor. The above conclusions are obtained through the output results of the debug version. The display and analysis of assembly code is omitted here.

 

If the virtual keyword of the P2 destructor is removed, an error will pop up when you run the above Code. Therefore, the compiler will analyze the P2 pointer value as an actual P2 object address, that is, it will try to free the address value. Obviously, this is wrong. In debug mode, the following assertion fail dialog box is displayed:

 

  

 

Therefore, from the above example, we can see why the fictitious functions of the class should be defined as virtual functions. In Objective c ++, this is the case. If the fictitious function is not virtual, the object may be semi-destructed. Of course, for a common single inherited object, if the instance has only one virtual function table pointer, if the child classes are basic data types and do not require additional processing, this will not actually cause any problems. When memory is allocated, the information block before the memory already describes the memory size. Therefore, there will be no error in releasing the memory. However, if the member of the subclass object needs to be released, a problem may occur. For example, if a Member points to the dynamically applied memory, it is obvious that they will become memory leaks.

 

  Conclusion:

 

Through the above analysis, we can see that,

 

(1) convert pointer Types Between Inherited types. The Compiler adds address adjustment during conversion.

 

(2) When multiple parent classes exist and the parent-class fictitious functions are virtual functions, the sub-objects are offset from the base address of the object, therefore, the compiler inserts a trunk code for each parent class with an offset (not at the top of the parent class list) and first adjusts the address to the actual object address, then jump to the destructor of the actual object to ensure that the object is correctly destructed.

 

Additional discussions:

 

In the second example, the compiler also adjusts the relevant address in the C1 constructor and destructor. For example, in the constructor of C1, the compiler is responsible for inserting the call to the constructor of all the parent classes of C1 (the constructor is only responsible for initializing the input object address, not responsible for memory allocation/release ). Since the P2 and P3 perspectives are offset from the C1 address of the object, when calling the P2 and P3 constructor, the object address is adjusted accordingly to the corresponding perspective, which is obvious. The following is an disassembly snippet of the VC debug version of the C1 constructor:

 

We can see that when the constructors P1, P2, and P3 are called respectively, the constructor actually fills the address of the virtual function table in the object header (P2, the P3 constructor is filled with the actual P2, P3 virtual function table address), and then the compiler is responsible for the part that is assigned a value to the virtual function table pointer of P1, P2, and P3. At this time, the virtual function table pointer of P1 actually points to the virtual function table of C1. The virtual function tables from the P2 and P3 perspectives point to the virtual function tables customized for C1 (these custom virtual function tables only have a special destructor entry, the other parts are the same as those in the original virtual function table ).

Mov [ebp + var_14], ecxmov ecx, [ebp + var_14] call sub_4110AA; call P1_Constructormov [ebp + var_4], 0mov ecx, [ebp + var_14] add ecx, 10 hcall sub_4110B9; call P2_Contructormov byte ptr [ebp + var_4], 1mov ecx, [ebp + var_14] add ecx, 20 hcall sub_4110BE; call P3_Contructormov eax, [ebp + var_14] mov dword ptr [eax], offset off_00006b8; reset P1/C1 vftable address mov eax, [ebp + var_14] mov dword ptr [eax + 10 h], offset off_00006a8; resetting P2 perspective vftable address mov eax, [ebp + var_14] mov dword ptr [eax + 20 h], offset off_0000698; resetting P3 perspective vftable address mov eax, [ebp + var_14]; the following content of the C1 constructor compiled by the user: mov dword ptr [eax + 30 h], 1mov eax, [ebp + var_14] mov dword ptr [eax + 34 h], 2mov eax, [ebp + var_14] mov dword ptr [eax + 38 h], 3mov eax, [ebp + var_14] mov dword ptr [eax + 3Ch], 4mov esi, esppush offset aC1Constructor _; "C1 constructor. \ n "call ds: printfadd esp, 4

 

If the destructor of the parent class P1 is non-virtual and the destructor of the subclass C1 is virtual, the behavior at this time is odd, that is, the virtual function table of C1 does not have the destructor of C1 (it seems that to make the subclass have a virtual destructor, its parent class must also have a virtual destructor first ). At this time, if we use the P1 pointer to parse the C1 object, we will actually only call the destructor of P1 (assuming that the object is allocated by the new operator) the delete operator is responsible for releasing the memory occupied by the object. That is, the result of the C1 object being semi-destructed. This is a good result that the virtual function table of P1 is overwritten by C1. If there is an offset between the object's Perspective (for example, delete C1 object using the P2 pointer and the destructor of P2 is non-virtual), the address when the memory is released during the delete operation, it is not the address returned during actual allocation, so it is certain that it will inevitably lead to a running error.

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.