C++ Pointer Adjustment

Introduction

C++ runtime polymorphisms can be achieved using a base class pointer pointing to the derived class object and the overridden functions implemented in the derived class object can be found and called via the base class vtable pointer. C++ pointer adjustment at runtime ensures that the base class vtable pointer is always found correctly.

In this blog post, I would like to quickly discuss C++ pointer adjustment using an example and compiler analysis tools.

C++ Pointer Adjustment

Example

This is a quick example that shows the C++ runtime polymorphisms and how the (base) pointer addresses got adjusted during runtime.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <iostream>

class Base1
{
public:
virtual void foo() { std::cout << "Base1::foo() is called." << std::endl; }
double m_v1;
char m_v2;
};

class Base2
{
public:
virtual void bar() { std::cout << "Base2::bar() is called." << std::endl; }
int m_v1;
};

class Derived : public Base1, public Base2
{
public:
void foo() override
{
std::cout << "Derived::foo() is called." << std::endl;
}
void bar() override
{
std::cout << "Derived::bar() is called." << std::endl;
}
int m_v1;
};

int main()
{
Derived derived{};
Derived* derived_ptr = &derived;

Base1* base1_ptr = derived_ptr;
// Derived::foo() is called.
base1_ptr->foo();

Base2* base2_ptr = derived_ptr;
// Derived::bar() is called.
base2_ptr->bar();

std::cout << "Base1 Ptr Address: " << base1_ptr << std::endl;
std::cout << "Base2 Ptr Address: " << base2_ptr << std::endl;
std::cout << "Derived Ptr Address: " << derived_ptr << std::endl;
// This will not work and should not be used.
// std::cout << static_cast<Base2*>(base1Ptr) << std::endl;
// In practice, need to check if dynamic cast is successful at runtime.
std::cout << "Base2 Ptr Dynamically Casted from Base1 Ptr Address: "
<< dynamic_cast<Base2*>(base1_ptr) << std::endl;
std::cout << "Base1 Ptr Dynamically Casted from Base2 Ptr Address: "
<< dynamic_cast<Base1*>(base2_ptr) << std::endl;
// C-casts are dangerous and incorrect in these use cases.
std::cout << "Base2 Ptr C-Casted from Base1 Ptr Address: "
<< (Base2*)(base1_ptr) << std::endl;
std::cout << "Base1 Ptr C-Casted from Base2 Ptr Address: "
<< (Base1*)(base2_ptr) << std::endl;

return 0;
}

Build and Run Example

We will build the example with debugging mode for later analysis. Running the example shows that the pointer addresses did get adjusted if correct implementations are used.

1
2
3
4
5
6
7
8
9
10
11
$ g++ -g pointer_adjustment.cpp -o pointer_adjustment
$ ./pointer_adjustment
Derived::foo() is called.
Derived::bar() is called.
Base1 Ptr Address: 0x7ffd5d192940
Base2 Ptr Address: 0x7ffd5d192958
Derived Ptr Address: 0x7ffd5d192940
Base2 Ptr Dynamically Casted from Base1 Ptr Address: 0x7ffd5d192958
Base1 Ptr Dynamically Casted from Base2 Ptr Address: 0x7ffd5d192940
Base2 Ptr C-Casted from Base1 Ptr Address: 0x7ffd5d192940
Base1 Ptr C-Casted from Base2 Ptr Address: 0x7ffd5d192958

Analyze Example

We will analyze the example built with debugging mode using pahole. It can be installed simply via sudo apt install pahole on Linux. The pahole analysis result is as follows.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
$ pahole pointer_adjustment
die__process_class: tag not supported 0x2f (template_type_parameter)!
struct typedef __va_list_tag __va_list_tag {
unsigned int gp_offset; /* 0 4 */
unsigned int fp_offset; /* 4 4 */
void * overflow_arg_area; /* 8 8 */
void * reg_save_area; /* 16 8 */

/* size: 24, cachelines: 1, members: 4 */
/* last cacheline: 24 bytes */
};
struct _IO_FILE {
int _flags; /* 0 4 */

/* XXX 4 bytes hole, try to pack */

char * _IO_read_ptr; /* 8 8 */
char * _IO_read_end; /* 16 8 */
char * _IO_read_base; /* 24 8 */
char * _IO_write_base; /* 32 8 */
char * _IO_write_ptr; /* 40 8 */
char * _IO_write_end; /* 48 8 */
char * _IO_buf_base; /* 56 8 */
/* --- cacheline 1 boundary (64 bytes) --- */
char * _IO_buf_end; /* 64 8 */
char * _IO_save_base; /* 72 8 */
char * _IO_backup_base; /* 80 8 */
char * _IO_save_end; /* 88 8 */
struct _IO_marker * _markers; /* 96 8 */
struct _IO_FILE * _chain; /* 104 8 */
int _fileno; /* 112 4 */
int _flags2; /* 116 4 */
__off_t _old_offset; /* 120 8 */
/* --- cacheline 2 boundary (128 bytes) --- */
short unsigned int _cur_column; /* 128 2 */
signed char _vtable_offset; /* 130 1 */
char _shortbuf[1]; /* 131 1 */

/* XXX 4 bytes hole, try to pack */

_IO_lock_t * _lock; /* 136 8 */
__off64_t _offset; /* 144 8 */
struct _IO_codecvt * _codecvt; /* 152 8 */
struct _IO_wide_data * _wide_data; /* 160 8 */
struct _IO_FILE * _freeres_list; /* 168 8 */
void * _freeres_buf; /* 176 8 */
size_t __pad5; /* 184 8 */
/* --- cacheline 3 boundary (192 bytes) --- */
int _mode; /* 192 4 */
char _unused2[20]; /* 196 20 */

/* size: 216, cachelines: 4, members: 29 */
/* sum members: 208, holes: 2, sum holes: 8 */
/* last cacheline: 24 bytes */
};
struct tm {
int tm_sec; /* 0 4 */
int tm_min; /* 4 4 */
int tm_hour; /* 8 4 */
int tm_mday; /* 12 4 */
int tm_mon; /* 16 4 */
int tm_year; /* 20 4 */
int tm_wday; /* 24 4 */
int tm_yday; /* 28 4 */
int tm_isdst; /* 32 4 */

/* XXX 4 bytes hole, try to pack */

long int tm_gmtoff; /* 40 8 */
const char * tm_zone; /* 48 8 */

/* size: 56, cachelines: 1, members: 11 */
/* sum members: 52, holes: 1, sum holes: 4 */
/* last cacheline: 56 bytes */
};
struct lconv {
char * decimal_point; /* 0 8 */
char * thousands_sep; /* 8 8 */
char * grouping; /* 16 8 */
char * int_curr_symbol; /* 24 8 */
char * currency_symbol; /* 32 8 */
char * mon_decimal_point; /* 40 8 */
char * mon_thousands_sep; /* 48 8 */
char * mon_grouping; /* 56 8 */
/* --- cacheline 1 boundary (64 bytes) --- */
char * positive_sign; /* 64 8 */
char * negative_sign; /* 72 8 */
char int_frac_digits; /* 80 1 */
char frac_digits; /* 81 1 */
char p_cs_precedes; /* 82 1 */
char p_sep_by_space; /* 83 1 */
char n_cs_precedes; /* 84 1 */
char n_sep_by_space; /* 85 1 */
char p_sign_posn; /* 86 1 */
char n_sign_posn; /* 87 1 */
char int_p_cs_precedes; /* 88 1 */
char int_p_sep_by_space; /* 89 1 */
char int_n_cs_precedes; /* 90 1 */
char int_n_sep_by_space; /* 91 1 */
char int_p_sign_posn; /* 92 1 */
char int_n_sign_posn; /* 93 1 */

/* size: 96, cachelines: 2, members: 24 */
/* padding: 2 */
/* last cacheline: 32 bytes */
};
struct _G_fpos_t {
__off_t __pos; /* 0 8 */
__mbstate_t __state; /* 8 8 */

/* size: 16, cachelines: 1, members: 2 */
/* last cacheline: 16 bytes */
};
class Derived : public Base1, public Base2 {
public:

/* class Base1 <ancestor>; */ /* 0 24 */

/* XXX last struct has 7 bytes of padding */

/* class Base2 <ancestor>; */ /* 24 16 */

/* XXX last struct has 4 bytes of padding */
/* XXX 65532 bytes hole, try to pack */
void ~Derived(class Derived *, int);

void Derived(class Derived *, );

void Derived(class Derived *, const class Derived &);

void Derived(class Derived *);

virtual void foo(class Derived *);

virtual void bar(class Derived *);


int m_v1; /* 36 4 */
/* vtable has 2 entries: {
[0] = foo((null)),
[1] = bar((null)),
} */
/* size: 40, cachelines: 1, members: 3 */
/* sum members: 4, holes: 1, sum holes: 65532 */
/* paddings: 2, sum paddings: 11 */
/* last cacheline: 40 bytes */

/* BRAIN FART ALERT! 40 bytes != 4 (member bytes) + 0 (member bits) + 65532 (byte holes) + 0 (bit holes), diff = -523968 bits */
};
class Base1 {
public:

void ~Base1(class Base1 *, int);

void Base1(class Base1 *, );

void Base1(class Base1 *, const class Base1 &);

void Base1(class Base1 *);

int ()(void) * * _vptr.Base1; /* 0 8 */
virtual void foo(class Base1 *);

double m_v1; /* 8 8 */
char m_v2; /* 16 1 */
/* vtable has 1 entries: {
[0] = foo((null)),
} */
/* size: 24, cachelines: 1, members: 3 */
/* padding: 7 */
/* last cacheline: 24 bytes */
};
class Base2 {
public:

void ~Base2(class Base2 *, int);

void Base2(class Base2 *, );

void Base2(class Base2 *, const class Base2 &);

void Base2(class Base2 *);

int ()(void) * * _vptr.Base2; /* 0 8 */
virtual void bar(class Base2 *);

int m_v1; /* 8 4 */
/* vtable has 1 entries: {
[0] = bar((null)),
} */
/* size: 16, cachelines: 1, members: 2 */
/* padding: 4 */
/* last cacheline: 16 bytes */
};

On the stack, the first element of a Base1 object is the vtable pointer _vptr.Base1 and it takes 8 bytes. The base size of a Base1 object is sizeof(Base1::_vptr.Base1) + sizeof(Base1::m_v1) + sizeof(Base1::m_v2) = 8 + 8 + 1 = 17 bytes, and it is padded to 24 bytes because of the 8 byte alignment requirement for the class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Base1 {
public:

void ~Base1(class Base1 *, int);

void Base1(class Base1 *, );

void Base1(class Base1 *, const class Base1 &);

void Base1(class Base1 *);

int ()(void) * * _vptr.Base1; /* 0 8 */
virtual void foo(class Base1 *);

double m_v1; /* 8 8 */
char m_v2; /* 16 1 */
/* vtable has 1 entries: {
[0] = foo((null)),
} */
/* size: 24, cachelines: 1, members: 3 */
/* padding: 7 */
/* last cacheline: 24 bytes */
};

Similarly, the first element of a Base2 object is the vtable pointer _vptr.Base2 and it takes 8 bytes. The base size of a Base2 object is sizeof(Base1::_vptr.Base2) + sizeof(Base1::m_v1) = 8 + 4 = 12 bytes, and it is padded to 16 bytes because of the 8 byte alignment requirement for the class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base2 {
public:

void ~Base2(class Base2 *, int);

void Base2(class Base2 *, );

void Base2(class Base2 *, const class Base2 &);

void Base2(class Base2 *);

int ()(void) * * _vptr.Base2; /* 0 8 */
virtual void bar(class Base2 *);

int m_v1; /* 8 4 */
/* vtable has 1 entries: {
[0] = bar((null)),
} */
/* size: 16, cachelines: 1, members: 2 */
/* padding: 4 */
/* last cacheline: 16 bytes */
};

The memory layout of a Derived object on the stack is the stack content of a Base1 object which takes 24 bytes followed by a Base2 object which takes 16 bytes followed by the Derived member objects. The base size of a Derived object is supposed to be sizeof(Base1) + sizeof(Base2) + sizeof(Derived::m_v1) = 24 + 16 + 4 = 44 bytes and it will be further padded to 48 bytes because of the alignment requirement. However, we could see that the actual size of a Derived object is just 40 bytes. This is actually due to a compiler optimization. Because a Base2 object will add 4 bytes for padding, and it happens to fit the Derived::m_v1 which is of int type, thus Derived::m_v1 is placed to the place where originally the padded bytes are for a Base2 object. The pahole analysis result also tells us that the offset of Derived::m_v1 is 36 instead of 40.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Derived : public Base1, public Base2 {
public:

/* class Base1 <ancestor>; */ /* 0 24 */

/* XXX last struct has 7 bytes of padding */

/* class Base2 <ancestor>; */ /* 24 16 */

/* XXX last struct has 4 bytes of padding */
/* XXX 65532 bytes hole, try to pack */
void ~Derived(class Derived *, int);

void Derived(class Derived *, );

void Derived(class Derived *, const class Derived &);

void Derived(class Derived *);

virtual void foo(class Derived *);

virtual void bar(class Derived *);


int m_v1; /* 36 4 */
/* vtable has 2 entries: {
[0] = foo((null)),
[1] = bar((null)),
} */
/* size: 40, cachelines: 1, members: 3 */
/* sum members: 4, holes: 1, sum holes: 65532 */
/* paddings: 2, sum paddings: 11 */
/* last cacheline: 40 bytes */

/* BRAIN FART ALERT! 40 bytes != 4 (member bytes) + 0 (member bits) + 65532 (byte holes) + 0 (bit holes), diff = -523968 bits */
};

Anyhow, we would expect that a Base1 pointer to the Derived object will be the same as a Derived pointer to the Derived object, a Base2 pointer to the Derived object will be 24 bytes larger than a Base1 pointer to the Derived object. In fact, our program print out has verified this. Notice that hexadecimal subtraction 0x7ffd5d192958 - 0x7ffd5d192940 results in 24 decimal.

1
2
3
4
5
6
7
8
9
10
11
$ g++ -g pointer_adjustment.cpp -o pointer_adjustment
$ ./pointer_adjustment
Derived::foo() is called.
Derived::bar() is called.
Base1 Ptr Address: 0x7ffd5d192940
Base2 Ptr Address: 0x7ffd5d192958
Derived Ptr Address: 0x7ffd5d192940
Base2 Ptr Dynamically Casted from Base1 Ptr Address: 0x7ffd5d192958
Base1 Ptr Dynamically Casted from Base2 Ptr Address: 0x7ffd5d192940
Base2 Ptr C-Casted from Base1 Ptr Address: 0x7ffd5d192940
Base1 Ptr C-Casted from Base2 Ptr Address: 0x7ffd5d192958

In our case, the two vtables from the two base classes are distinct. The Derived::foo() is not in the Base2 vtable and the Derived::bar() is not in the Base1 vtable because the base classes did not declare them respectively. When the overridden functions are called via a Base1 pointer to the Derived object, the Base1::_vptr.Base1 vtable pointer will be used for finding the overridden function pointers, when the overridden functions are called via a Base2 pointer to the Derived object, the Base2::_vptr.Base1 vtable will be used for finding the overridden function pointers.

Without pointer adjustment, say, a Base1 pointer and a Base2 pointer both points to the beginning of the Derived object and the Base1::_vptr.Base1 vtable pointer will be used for finding the overridden functions, the Base2 pointer overridden function call will not work as expected and result in undefined behaviors.

Conclusion

The pointer adjustment ensures that base pointers can correctly point to the member objects it owns, including the vtable pointer. This is critical for ensuring the runtime polymorphic behaviors via the correct vtable pointers are always expected.

Author

Lei Mao

Posted on

06-26-2023

Updated on

06-26-2023

Licensed under


Comments