「现代C++设计魅力」虚函数继承-thunk技术初探

简介:工作中使用LLDB调试器调试这一段C++多继承程序的时候,发现通过lldb print(expression命令的别名) 命令获取的指针地址和实际理解的C++的内存模型的地址不一样。那么到底是什么原因呢?

作者 | 扬阜
来源 | 阿里技术公众号

一 问题背景

1 实践验证

工作中使用LLDB调试器调试这一段C++多继承程序的时候,发现通过lldb print(expression命令的别名) 命令获取的指针地址和实际理解的C++的内存模型的地址不一样。那么到底是什么原因呢?程序如下:

class Base {
public:Base(){}
protected:float x;
};
class VBase {
public:VBase(){}virtual void test(){};virtual void foo(){};
protected:float x;
};
class VBaseA: public VBase {
public:VBaseA(){}virtual void test(){}virtual void foo(){};
protected:float x;
};
class VBaseB: public VBase  {
public:VBaseB(){}virtual void test(){printf("test \n");}virtual void foo(){};
protected:float x;
};
class VDerived : public VBaseA, public Base, public VBaseB {
public:VDerived(){}virtual void test(){}virtual void foo(){};
protected:float x;
};
int  main(int argc, char *argv[])
{VDerived *pDerived = new VDerived(); //0x0000000103407f30Base  *pBase = (Base*)pDerived; //0x0000000103407f40VBaseA *pvBaseA = static_cast< VBaseA*>(pDerived);//0x0000000103407f30VBaseB  *pvBaseB = static_cast< VBaseB*>(pDerived);//0x0000000103407f30 这里应该为0x0000000103407f48,但是显示的是0x0000000103407f30unsigned long pBaseAddressbase = (unsigned long)pBase;unsigned long pvBaseAAddressbase = (unsigned long)pvBaseA;unsigned long pvBaseBAddressbase = (unsigned long)pvBaseB;pvBaseB->test();
}

通过lldb print命令获取的地址如下图:

正常理解的C++内存模型

由于我使用的是x86_64的mac系统,所以指针是8字节对齐,align=8。

按正常的理解的C++内存模型:pDerived转换为Base 类型pBase,地址偏移了16,是没问题的。

pDerived转化为VBaseA,由于共用了首地址为0x0000000103407f30,一样可以理解。pDerived转化为Base,地址偏移了16个字节(sizeof(VBaseA))为0x0000000103407f40,也是符合预期的。

但是pDerived转化为VBase 类型pBaseB内存地址应该偏移24,为0x0000000103407f48;而不是0x0000000103407f30(对象的首地址),这个到底是什么原因引起的的呢?

2 验证引发的猜测

对于上面的这段代码

Base 类中没有虚函数,VBaseB 中有虚函数test和foo,猜测如下

1.不含有虚函数的(不含有虚表的)基类的指针,在类型转换时编译器对地址按照实际偏移。

2.含有虚函数的(含有虚表的)基类指针,在类型转换时,编译器实际上没有做地址的偏移,还是指向派生类,并没有指向实际的VBaseB类型。

二 现象带来的问题

1.有虚函数的(含有虚表的)基类指针,在派生类类型转换为有虚函数的基类时,编译器背后有做真实的地址偏移吗?

2.如果做了偏移

  • 那C++中在通过基类指针调用派生类重写的虚函数以及通过派生类指针调用虚函数的时候,编译器是如何保证这两种调用this指针的值是一样的,以确保调用的正确性的?
  • 那为什么LLDB expression获取的地址是派生类对象的首地址呢?

3.如果没有做偏移,那是如何通过派生类的指针调用基类成员变量和函数的?

三 现象核心原因

  1. 编译器背后和普通的非虚函数继承一样,也做了指针的偏移。
  2. 做了指针偏移,C++ 中基类对象指针调用派生类对象时,编译器通过thunk技术来实现每次参数调用和参数返回this地址的调整。
  3. LLDB expression显示的是派生类对象的首地址(0x0000000103407f30),而不是偏移后基类对象的首地址(0x0000000103407f48),是由于LLDB调试器在expression向用户展示的时候,对于虚函数继承的基类指针LLDB内部会通过summary format来对要获取的结果进行格式化。summary format时,会根据当前的内存地址获取C++运行时的动态类型和地址,来向用户展示。

四 证实结论过程

1 指针类型转换时编译器是否做了偏移?

汇编指令分析

有虚函数的(含有虚表的)基类指针,在派生类类型转换为有虚函数的基类时,编译器背后有做真实的地址偏移吗?

基于上面的猜测,通过下面运行时反汇编的程序,来验证上面的猜测:

在开始反汇编程序之前,有一些下面要用到的汇编知识的普及。如果熟悉,可以忽略跳过。

注意:由于小编使用的是mac操作系统,所以处理器使用的是AT&T语法;和Intel语法不一样。

AT&T语法的指令是从左到右,第一个是源操作数,第二个是目的操作数,比如:

movl %esp, %ebp  //movl是指令名称。%则表明esp和ebp是寄存器.在AT&T语法中, 第一个是源操作数,第二个是目的操作数。

而Intel指令是从右到左,第二个是源操作数,第一个是目的操作数

MOVQ EBP, ESP //interl手册,你会看到是没有%的intel语法, 它的操作数顺序刚好相反

在x86_64的寄存器调用约定规定中

1.第一个参数基本上放在:RDI/edi寄存器,第二个参数:RSI/esi寄存器,第三个参数:RDX寄存器,第四个参数:RCD寄存器,第五个参数:R8寄存器,第六个参数:R9 寄存器;

2.如果超过六个参数在函数里就会通过栈来访问额外的参数;

3.函数返回值一般放在eax寄存器,或者rax寄存器。

下面使用的mac Unix操作系统,本文用到的汇编指令都是AT&T语法,在函数传参数时的第一个参数都放在RDI寄存器中。

下面是上面的main程序从开始执行到退出程序的所有汇编程序

通过上看的汇编代码我们发现编译器在做类型转换的时候不管是继承的基类有虚函数,还是没有虚函数,编译器都会做实际的指针偏移,偏移到实际的基类对象的地址,证明上面的猜测是错误的。编译器在类型转换的时候不区分有没有虚函数,都是实际做了偏移的。

内存分析

上面的猜测,后来我通过LLDB调试器提供的:memory read ptr(memory read 命令缩写 x )得到了验证

(lldb) memory read pDerived
0x103407f30: 40 40 00 00 01 00 00 00 00 00 00 00 00 00 00 00  @@..............
0x103407f40: 10 00 00 00 00 00 00 00 60 40 00 00 01 00 00 00  ........`@......
(lldb) memory read pvBaseB
0x103407f48: 60 40 00 00 01 00 00 00 00 00 00 00 00 00 00 00  `@..............
0x103407f58: de 2d 05 10 00 00 00 00 00 00 00 00 00 00 00 00  .-..............

我们发现不同类型的指针 在内存中确实读取到的内容分别是pDerived:0x103407f30 pvBaseB:0x103407f48内存地址都不一样;都是实际偏移后地址。

2 虚函数调用如何保证this的值一致的呢?

那既然内容中的真实地址是偏移后的,派生类重写了基类的虚函数,在通过基类指针调用派生类重新的虚函数的时候和通过派生类调用自身实现的虚函数的时候,编译器是如何保证这两种调用this指针的值是一样的,来确保调用的正确性的?

在网上查阅资料得知:C++在调用函数的时候, 编译器通过thunk技术对this指针的内容做了调整,使其指向正确的内存地址。那么什么是thunk技术?编译器是如何实现的呢?

虚函数调用汇编指令分析

通过上面main函数不难发现的pvBaseB->test() 的反汇编:

  pBaseB->test();0x100003c84 < +244>: movq   -0x40(%rbp), %rax    //-x40存方的是pBaseB指针的内容,这里取出pBaseB指向的地址0x100003c88 < +248>: movq   (%rax), %rcx         //然后将 rax的内容赋值给rcx0x100003c8b < +251>: movq   %rax, %rdi           // 之后再将rax的值给到rdi寄存器:我们都知道,rdi寄存器是函数调用的第一个参数,这里的this是基类的地址
->  0x100003c8e < +254>: callq  *(%rcx)              // 在这里取出rcx的地址,然后通过*(rcx) 间接调用rcx中存的地址

我们再跳到VDerived::test函数的汇编实现, 在这里通过lldb的命令:register read rdi 查看函数的第一个传参,也就是 this的地址,已经是派生类的地址了,不是调用前基类的地址

testCPPVirtualMemeory`VDerived::test:0x100003e00 < +0>:  pushq  %rbp       //   栈低指针压栈   0x100003e01 < +1>:  movq   %rsp, %rbp //  将BP指针指向SP,因为上一级函数的栈顶指针是下一级函数的栈底指针0x100003e04 < +4>:  subq   $0x10, %rsp  // 开始函数栈帧空间0x100003e08 < +8>:  movq   %rdi, -0x8(%rbp)      //  将函数第一个参数入栈,也就是this 指针
->  0x100003e0c < +12>: leaq   0x15c(%rip), %rdi         ; "test\n"  0x100003e13 < +19>: movb   $0x0, %al0x100003e15 < +21>: callq  0x100003efc               ; symbol stub for: printf0x100003e1a < +26>: addq   $0x10, %rsp //回收栈空间0x100003e1e < +30>: popq   %rbp        //出栈 指回上一层 rbp0x100003e1f < +31>: retq               //指向下一条命令

通过上面的汇编我们分析,编译器在调用虚函数表中的函数时,是通过 *(%rcx) 间接寻址,然后中间做了某一个操作,跳到 test的实现,那么这个过程中thunk做了什么操作呢?

llvm-thunk源代码分析

小编使用的IDE都使用的是LLVM编译器,于是通过翻看LLVM的源码找到了答案: 在VTableBuilder.cpp的AddMethods函数,小编找到了答案,描述如下:

  // Now go through all virtual member functions and add them to the current// vftable. This is done by//  - replacing overridden methods in their existing slots, as long as they//    don't require return adjustment; calculating This adjustment if needed.//  - adding new slots for methods of the current base not present in any//    sub-bases;//  - adding new slots for methods that require Return adjustment.// We keep track of the methods visited in the sub-bases in MethodInfoMap.

编译器在编译的时候会判断基类的虚函数派生类有没有覆盖,如果有实现的时候,则动态替换虚函数表中的地址为派生类的地址,同时:

1.会计算调用时this指针的地址是否需要调整,如果需要调整的话,会为当前的方法开辟一块新的内存空间;

2.也会为需要this返回值的函数开辟一块新的内存空间;

代码如下:

void VFTableBuilder::AddMethods(BaseSubobject Base, unsigned BaseDepth,const CXXRecordDecl *LastVBase,BasesSetVectorTy &VisitedBases) {const CXXRecordDecl *RD = Base.getBase();if (!RD->isPolymorphic())return;const ASTRecordLayout &Layout = Context.getASTRecordLayout(RD);// See if this class expands a vftable of the base we look at, which is either// the one defined by the vfptr base path or the primary base of the current// class.const CXXRecordDecl *NextBase = nullptr, *NextLastVBase = LastVBase;CharUnits NextBaseOffset;if (BaseDepth < WhichVFPtr.PathToIntroducingObject.size()) {NextBase = WhichVFPtr.PathToIntroducingObject[BaseDepth];if (isDirectVBase(NextBase, RD)) {NextLastVBase = NextBase;NextBaseOffset = MostDerivedClassLayout.getVBaseClassOffset(NextBase);} else {NextBaseOffset =Base.getBaseOffset() + Layout.getBaseClassOffset(NextBase);}} else if (const CXXRecordDecl *PrimaryBase = Layout.getPrimaryBase()) {assert(!Layout.isPrimaryBaseVirtual() &&"No primary virtual bases in this ABI");NextBase = PrimaryBase;NextBaseOffset = Base.getBaseOffset();}if (NextBase) {AddMethods(BaseSubobject(NextBase, NextBaseOffset), BaseDepth + 1,NextLastVBase, VisitedBases);if (!VisitedBases.insert(NextBase))llvm_unreachable("Found a duplicate primary base!");}SmallVector< const CXXMethodDecl*, 10> VirtualMethods;// Put virtual methods in the proper order.GroupNewVirtualOverloads(RD, VirtualMethods);// Now go through all virtual member functions and add them to the current// vftable. This is done by//  - replacing overridden methods in their existing slots, as long as they//    don't require return adjustment; calculating This adjustment if needed.//  - adding new slots for methods of the current base not present in any//    sub-bases;//  - adding new slots for methods that require Return adjustment.// We keep track of the methods visited in the sub-bases in MethodInfoMap.for (const CXXMethodDecl *MD : VirtualMethods) {FinalOverriders::OverriderInfo FinalOverrider =Overriders.getOverrider(MD, Base.getBaseOffset());const CXXMethodDecl *FinalOverriderMD = FinalOverrider.Method;const CXXMethodDecl *OverriddenMD =FindNearestOverriddenMethod(MD, VisitedBases);ThisAdjustment ThisAdjustmentOffset;bool ReturnAdjustingThunk = false, ForceReturnAdjustmentMangling = false;CharUnits ThisOffset = ComputeThisOffset(FinalOverrider);ThisAdjustmentOffset.NonVirtual =(ThisOffset - WhichVFPtr.FullOffsetInMDC).getQuantity();if ((OverriddenMD || FinalOverriderMD != MD) &&WhichVFPtr.getVBaseWithVPtr())CalculateVtordispAdjustment(FinalOverrider, ThisOffset,ThisAdjustmentOffset);unsigned VBIndex =LastVBase ? VTables.getVBTableIndex(MostDerivedClass, LastVBase) : 0;if (OverriddenMD) {// If MD overrides anything in this vftable, we need to update the// entries.MethodInfoMapTy::iterator OverriddenMDIterator =MethodInfoMap.find(OverriddenMD);// If the overridden method went to a different vftable, skip it.if (OverriddenMDIterator == MethodInfoMap.end())continue;MethodInfo &OverriddenMethodInfo = OverriddenMDIterator->second;VBIndex = OverriddenMethodInfo.VBTableIndex;// Let's check if the overrider requires any return adjustments.// We must create a new slot if the MD's return type is not trivially// convertible to the OverriddenMD's one.// Once a chain of method overrides adds a return adjusting vftable slot,// all subsequent overrides will also use an extra method slot.ReturnAdjustingThunk = !ComputeReturnAdjustmentBaseOffset(Context, MD, OverriddenMD).isEmpty() ||OverriddenMethodInfo.UsesExtraSlot;if (!ReturnAdjustingThunk) {// No return adjustment needed - just replace the overridden method info// with the current info.MethodInfo MI(VBIndex, OverriddenMethodInfo.VFTableIndex);MethodInfoMap.erase(OverriddenMDIterator);assert(!MethodInfoMap.count(MD) &&"Should not have method info for this method yet!");MethodInfoMap.insert(std::make_pair(MD, MI));continue;}// In case we need a return adjustment, we'll add a new slot for// the overrider. Mark the overridden method as shadowed by the new slot.OverriddenMethodInfo.Shadowed = true;// Force a special name mangling for a return-adjusting thunk// unless the method is the final overrider without this adjustment.ForceReturnAdjustmentMangling =!(MD == FinalOverriderMD && ThisAdjustmentOffset.isEmpty());} else if (Base.getBaseOffset() != WhichVFPtr.FullOffsetInMDC ||MD->size_overridden_methods()) {// Skip methods that don't belong to the vftable of the current class,// e.g. each method that wasn't seen in any of the visited sub-bases// but overrides multiple methods of other sub-bases.continue;}// If we got here, MD is a method not seen in any of the sub-bases or// it requires return adjustment. Insert the method info for this method.MethodInfo MI(VBIndex,HasRTTIComponent ? Components.size() - 1 : Components.size(),ReturnAdjustingThunk);assert(!MethodInfoMap.count(MD) &&"Should not have method info for this method yet!");MethodInfoMap.insert(std::make_pair(MD, MI));// Check if this overrider needs a return adjustment.// We don't want to do this for pure virtual member functions.BaseOffset ReturnAdjustmentOffset;ReturnAdjustment ReturnAdjustment;if (!FinalOverriderMD->isPure()) {ReturnAdjustmentOffset =ComputeReturnAdjustmentBaseOffset(Context, FinalOverriderMD, MD);}if (!ReturnAdjustmentOffset.isEmpty()) {ForceReturnAdjustmentMangling = true;ReturnAdjustment.NonVirtual =ReturnAdjustmentOffset.NonVirtualOffset.getQuantity();if (ReturnAdjustmentOffset.VirtualBase) {const ASTRecordLayout &DerivedLayout =Context.getASTRecordLayout(ReturnAdjustmentOffset.DerivedClass);ReturnAdjustment.Virtual.Microsoft.VBPtrOffset =DerivedLayout.getVBPtrOffset().getQuantity();ReturnAdjustment.Virtual.Microsoft.VBIndex =VTables.getVBTableIndex(ReturnAdjustmentOffset.DerivedClass,ReturnAdjustmentOffset.VirtualBase);}}AddMethod(FinalOverriderMD,ThunkInfo(ThisAdjustmentOffset, ReturnAdjustment,ForceReturnAdjustmentMangling ? MD : nullptr));}
}

通过上面代码分析,在this 需要调整的时候,都是通过AddMethod(FinalOverriderMD,ThunkInfo(ThisAdjustmentOffset, ReturnAdjustment,ForceReturnAdjustmentMangling ? MD : nullptr))函数来添加一个ThunkInfo的结构体,ThunkInfo在结构体(实现在ABI.h)如下:

struct ThunkInfo {/// The \c this pointer adjustment.ThisAdjustment This;/// The return adjustment.ReturnAdjustment Return;/// Holds a pointer to the overridden method this thunk is for,/// if needed by the ABI to distinguish different thunks with equal/// adjustments. Otherwise, null./// CAUTION: In the unlikely event you need to sort ThunkInfos, consider using/// an ABI-specific comparator.const CXXMethodDecl *Method;ThunkInfo() : Method(nullptr) { }ThunkInfo(const ThisAdjustment &This, const ReturnAdjustment &Return,const CXXMethodDecl *Method = nullptr): This(This), Return(Return), Method(Method) {}friend bool operator==(const ThunkInfo &LHS, const ThunkInfo &RHS) {return LHS.This == RHS.This && LHS.Return == RHS.Return &&LHS.Method == RHS.Method;}bool isEmpty() const {return This.isEmpty() && Return.isEmpty() && Method == nullptr;}
};}

Thunkinfo的结构体有一个method,存放函数的真正实现,This和Return记录this需要调整的信息,然后在生成方法的时候,根据这些信息,编译器自动插入thunk函数的信息,通过ItaniumMangleContextImpl::mangleThunk(const CXXMethodDecl *MD,const ThunkInfo &Thunk,raw_ostream &Out)的函数,我们得到了证实,函数如下:

(mangle和demangle:将C++源程序标识符(original C++ source identifier)转换成C++ ABI标识符(C++ ABI identifier)的过程称为mangle;相反的过程称为demangle。wiki)

void ItaniumMangleContextImpl::mangleThunk(const CXXMethodDecl *MD,const ThunkInfo &Thunk,raw_ostream &Out) {//  < special-name> ::= T < call-offset> < base encoding>//                      # base is the nominal target function of thunk//  < special-name> ::= Tc < call-offset> < call-offset> < base encoding>//                      # base is the nominal target function of thunk//                      # first call-offset is 'this' adjustment//                      # second call-offset is result adjustmentassert(!isa< CXXDestructorDecl>(MD) &&"Use mangleCXXDtor for destructor decls!");CXXNameMangler Mangler(*this, Out);Mangler.getStream() << "_ZT";if (!Thunk.Return.isEmpty())Mangler.getStream() << 'c';// Mangle the 'this' pointer adjustment.Mangler.mangleCallOffset(Thunk.This.NonVirtual,Thunk.This.Virtual.Itanium.VCallOffsetOffset);// Mangle the return pointer adjustment if there is one.if (!Thunk.Return.isEmpty())Mangler.mangleCallOffset(Thunk.Return.NonVirtual,Thunk.Return.Virtual.Itanium.VBaseOffsetOffset);Mangler.mangleFunctionEncoding(MD);
}

thunk汇编指令分析

至此,通过LLVM源码我们解开了thunk技术的真面目,那么我们通过反汇编程序来验证证实一下, 这里使用objdump 或者逆向利器 hopper都可以,小编使用的是hopper,汇编代码如下:

1.我们先来看编译器实现的thunk 版的test函数

派生类实现的test函数

编译器实现的thunk版的test函数

2.通过上面两张截图我们发现

编译器实现的thunk的test函数地址为0x100003e30

派生类实现的test函数地址为0x100003e00

下面我们来看下派生类的虚表中存的真实地址是那一个

通过上图我们可以看到:派生类的虚表中存的真实地址为编译器动态添加的thunk函数的地址0x100003e30。

上面分析的*(rcx)间接寻址:就是调用thunk函数的实现,然后在thunk中去调用真正的派生类覆盖的函数。

在这里我们可以确定的 thunk技术:

就是编译器在编译的时候,遇到调用this和返回值this需要调整的地方,动态的加入对应的thunk版的函数,在thunk函数的内部实现this的偏移调整,和调用派生类实现的虚函数;并将编译器实现的thunk函数的地址存入虚表中,而不是派生类实现的虚函数的地址。

thunk函数的内存布局

也可以确定对应的内存布局如下:

故(继承链中不是第一个)虚函数继承的基类指针的调用顺序为:

virtual-thunk和non-virtual-thunk

注意:在这里可以看到,内存中有两份VBase,在多继承中分为普通继承、虚函数继承、虚继承。虚继承主要是为了解决上面看到的问题:在内存中同时有两份Vbase 的内存,将上面的代码改动一下就会确保内存中的实例只有一份:

class VBaseA: public VBase 改成 class VBaseA: public virtual VBase

class VBaseB: public VBase 改成 class VBaseB: public virtual VBase

这样内存中的VBase就只有一分内存了。

到这里还有问题没有解答,就是上面截图里的thunk函数类型是:

我们发现thunk函数是 non-virtual-thunk类型,那对应的virtual-thunk是什么类型呢?

在解答这个问题之前我们现看下下面的例子?

public A {virtual void test() {}
}
public B {virtual void test1() {}
}
public C {virtual void test2() {}
}
public D : public virtual A, public virtual B, public C {virtual void test1() { // 这里实现的test1函数在 B类的虚函数表里就是virtual-trunk的类型}virtual void test2() { // 这里实现的test2函数在 C类的虚函数表示就是no-virtual-trunk的类型}
}

虚函数继承和虚继承相结合,且该类在派生类的继承链中不是第一个基类的时候,则该派生类实现的虚函数在编译器编译的时候,虚表里存放就是virtual-trunk类型。

只有虚函数继承的时候,且该类在派生类的继承链中不是第一个基类的时候,则该派生类实现的虚函数在编译器编译的时候,虚表里存放就是no-virtual-trunk类型。

3 为什么LLDB调试器显示的地址一样呢?

如果做了偏移,那为什么LLDB expression显示的地址是派生类对象的首地址呢?

到了现在了解了什么是thunk技术,还没有一个问题没有解决:就是LLDB调试的时候,显示的this的地址是基类偏移后的(派生类的地址),前面通过汇编分析编译器在类型转换的时候,做了真正的偏移,通过读取内存地址也发现是偏移后的真实地址,那lldb expression获取的地址为啥还是派生类的地址呢?由此可以猜测是LLDB调试器通过exppress 命令执行的时候做了类型的转换。

通过翻阅LLDB调试器的源码和LLDB说明文档,通过文档得知LLDB在每次拿到一个地址,需要向用户友好的展示的时候,首先需要通过summary format()进行格式化转换,格式化转化的依据是动态类型(lldb-getdynamictypeandaddress)的获取,在LLDB源码的bool ItaniumABILanguageRuntime::GetDynamicTypeAndAddress (lldb-summary-format)函数中找到了答案,代码如下

 // For Itanium, if the type has a vtable pointer in the object, it will be at// offset 0// in the object.  That will point to the "address point" within the vtable// (not the beginning of the// vtable.)  We can then look up the symbol containing this "address point"// and that symbol's name// demangled will contain the full class name.// The second pointer above the "address point" is the "offset_to_top".  We'll// use that to get the// start of the value object which holds the dynamic type.
bool ItaniumABILanguageRuntime::GetDynamicTypeAndAddress(ValueObject &in_value, lldb::DynamicValueType use_dynamic,TypeAndOrName &class_type_or_name, Address &dynamic_address,Value::ValueType &value_type) {// For Itanium, if the type has a vtable pointer in the object, it will be at// offset 0// in the object.  That will point to the "address point" within the vtable// (not the beginning of the// vtable.)  We can then look up the symbol containing this "address point"// and that symbol's name// demangled will contain the full class name.// The second pointer above the "address point" is the "offset_to_top".  We'll// use that to get the// start of the value object which holds the dynamic type.//class_type_or_name.Clear();value_type = Value::ValueType::eValueTypeScalar;// Only a pointer or reference type can have a different dynamic and static// type:if (CouldHaveDynamicValue(in_value)) {// First job, pull out the address at 0 offset from the object.AddressType address_type;lldb::addr_t original_ptr = in_value.GetPointerValue(&address_type);if (original_ptr == LLDB_INVALID_ADDRESS)return false;ExecutionContext exe_ctx(in_value.GetExecutionContextRef());Process *process = exe_ctx.GetProcessPtr();if (process == nullptr)return false;Status error;const lldb::addr_t vtable_address_point =process->ReadPointerFromMemory(original_ptr, error);if (!error.Success() || vtable_address_point == LLDB_INVALID_ADDRESS) {return false;}class_type_or_name = GetTypeInfoFromVTableAddress(in_value, original_ptr,vtable_address_point);if (class_type_or_name) {TypeSP type_sp = class_type_or_name.GetTypeSP();// There can only be one type with a given name,// so we've just found duplicate definitions, and this// one will do as well as any other.// We don't consider something to have a dynamic type if// it is the same as the static type.  So compare against// the value we were handed.if (type_sp) {if (ClangASTContext::AreTypesSame(in_value.GetCompilerType(),type_sp->GetForwardCompilerType())) {// The dynamic type we found was the same type,// so we don't have a dynamic type here...return false;}// The offset_to_top is two pointers above the vtable pointer.const uint32_t addr_byte_size = process->GetAddressByteSize();const lldb::addr_t offset_to_top_location =vtable_address_point - 2 * addr_byte_size;// Watch for underflow, offset_to_top_location should be less than// vtable_address_pointif (offset_to_top_location >= vtable_address_point)return false;const int64_t offset_to_top = process->ReadSignedIntegerFromMemory(offset_to_top_location, addr_byte_size, INT64_MIN, error);if (offset_to_top == INT64_MIN)return false;// So the dynamic type is a value that starts at offset_to_top// above the original address.lldb::addr_t dynamic_addr = original_ptr + offset_to_top;if (!process->GetTarget().GetSectionLoadList().ResolveLoadAddress(dynamic_addr, dynamic_address)) {dynamic_address.SetRawAddress(dynamic_addr);}return true;}}}return class_type_or_name.IsEmpty() == false;
}

通过上面代码分析可知,每次在通过LLDB 命令expression动态调用 指针地址的时候,LLDB 会去按照调试器默认的格式进行格式化,格式化的前提是动态获取到对应的类型和偏移后的地址;在碰到C++有虚表的时候,且不是虚表中的第一个基类指针的时候,就会使用指针上头的offset_to_top 获取到这个对应动态的类型和返回动态获取的该类型对象开始的地址。

五 总结

  1. 上面主要验证了在指针类型转换的时候,编译器内部做了真实的地址偏移;
  2. 通过上面的分析,我们得知编译器在函数调用时通过thunk技术动态调整入参this指针和返回值this指针,保证C++调用时this的正确性;
  3. 在通过LLDB expression获取非虚函数基类指针内容时,LLDB内部通过summary format进行格式化转换,格式化转化时会进行动态类型的获取。

六 工具篇

1 获取汇编程序

预处理->汇编

clang++ -E main.cpp -o main.i
clang++ -S main.i

objdump

objdump -S -C 可执行程序

反汇编利器: hopper

下载hopper,可执行程序拖入即可

Xcode

Xcode->Debug->Debug WorkFlow->Show disassembly

2 导出C++内存布局

Clang++编译器

clang++ -cc1 -emit-llvm -fdump-record-layouts -fdump-vtable-layouts main.cpp

七 参考文献

Dynamic types in LLDB
Variable Formatting — The LLDB Debugger
lldb/ItaniumABILanguageRuntime.cpp at bc19e289f759c26e4840aab450443d4a85071139 · llvm-mirror/lldb · GitHub
clang: lib/AST/VTableBuilder.cpp Source File
clang: include/clang/Basic/ABI.h Source File

相关技术:

llvm-virtual-thunk
llvm-no-virtual-thunk
lldb-summary-format
lldb-getdynamictypeandaddress

原文链接

本文为阿里云原创内容,未经允许不得转载。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/511522.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

万物互联时代到来,锐捷发布场景化无线零漫游方案

数字化和万物互联时代到来&#xff0c;物联网与 IoT 设备发展迅猛&#xff0c;以往只在办公区域主要由手机等移动设备使用的无线网络&#xff0c;正在接入更多核心业务生产、物流仓储等各类的生产设备。据分析机构 IDC 预测&#xff0c;无线网络优先是当下智能园区网络建设投资…

阿里云田涛涛:高效智能的云,CloudOps让运维更简单

简介&#xff1a;CloudOps:以应用为中心的自动化运维新趋势 12月21日&#xff0c;在阿里云弹性计算年度峰会上&#xff0c;阿里云弹性计算体验与控制系统负责人田涛涛发表了主题为《高效智能的云&#xff0c;CloudOps让运维更简单》的演讲&#xff0c;深度解读了云上运维新趋势…

打造南沙“强芯”,南沙首届IC Nansha大会召开

6月25日&#xff0c;2022 中国南沙国际集成电路产业论坛在广州南沙召开。本次峰会由广州南沙经济技术开发区管理委员会、广州市工业和信息化局主办&#xff1b;支持单位为广州湾区半导体产业集团有限公司、广东省集成电路行业协会、广州市半导体协会&#xff1b;广东省半导体及…

OpenAI开发者大会简介

文章目录 GPT-4 Turbo 昨天晚上 OpenAI的首届开发者大会召开 Sam Altman也做了公开演讲&#xff0c;应该说 这是继今年春天发布GPT-4之后 OpenAI在AI行业又创造的一个不眠夜 过去一年 ChatGPT绝对是整个科技领域最热的词汇 OpenAI 也依靠ChatGPT取得了惊人的成绩 ChatG…

阿里云贾少天:大规模云服务器高效使用及管理实践

简介&#xff1a;本篇内容分享了大规模云服务器高效使用及管理最佳实践。 2021年10月22日&#xff0c;在云栖大会的《云上运维最佳实践》分论坛&#xff0c;阿里云高级技术专家贾少天发表了主题为“大规模云服务器高效使用及管理最佳实践”的演讲&#xff0c;本篇内容根据他的…

发现新视界——视觉计算将如何改变生产方式

简介&#xff1a;本篇内容将从3个部分为读者介绍关于视觉计算如何改变生产方式&#xff0c;进一步阐述可视化业务方面的挑战及阿里云视觉计算的解决方案与优势。 编者按&#xff1a;在2021年10月举办的云栖大会的《数字孪生&Cloud XR技术助力产研创新论坛》上&#xff0c;…

容器监控指南:三剑客轻松实现 Docker 容器监控

作者 | Milan Mahat在本指南中&#xff0c;我们将学习如何使用 docker-compose 在容器中设置 cAdvisor&#xff0c;将其与 prometheus 连接&#xff0c;并通过 grafana 监控服务器的容器。CAdvisor 是一种流行的工具&#xff0c;用于收集容器的信息。它是 prometheus 和 grafan…

N个技巧,编写更高效 Dockerfile|云效工程师指北

简介&#xff1a;云原生时代下软件的构建和部署离不开容器技术。提到容器&#xff0c;几乎大家下意识都会联想到 Docker 。而 Docker 中有两个非常重要的概念&#xff0c;一个是Image&#xff08;镜像&#xff09;&#xff0c;一个是Container&#xff08;容器&#xff09;。前…

TDA-04D8变送器数据上报阿里云

简介&#xff1a;本文将以TDA-04D8变送器作为采集对象&#xff0c;使用海创微联采集控制系统对TDA-04D8变送器进行采集&#xff0c;然后将设备上的毛重、净重、皮重数据采集上传到阿里云物联网平台&#xff0c;阿里云物联网平台将数据实时可视化。 文章分为3部分&#xff1a; …

http ,怎么优雅的拒绝你

作者 | 奇伢来源 | 奇伢云存储典型问题&#xff1a;服务端优雅的拒绝今天分享一个后端编程的实际经验。这个问题来源于对象 S3 后端协议实现的技巧思考。场景&#xff1a;服务端不想接收 http 的 body 的时候&#xff0c;该怎么优雅的拒绝呢&#xff1f;什么意思&#xff1f;对…

企业物联网平台新版公共实例升级企业实例教程

简介&#xff1a;2021年7月30日企业物联网平台重磅升级&#xff0c;发布的新版公共实例支持一键升级企业版实例&#xff0c;本文将为大家介绍一键升级教程 一、企业版实例&#xff0c;企业用户首选 企业物联网平台 提供设备上云必备的基础服务&#xff0c;用户无需自建物联网…

【全观测系列】Elasticsearch应用性能监控实践

简介&#xff1a;本文介绍了应用性能监控的应用价值以及解决方案等。 1、什么是全观测&#xff1f; 要了解全观测&#xff0c;我们先看看传统运维存在哪些问题。 数据孤岛&#xff0c;分散在不同部门&#xff0c;分析排查故障困难&#xff1b;多个厂商的多种工具&#xff0c…

es实战-使用IK分词器进行词频统计

简介&#xff1a;通过IK分词器分词并生成词云。 本文主要介绍如何通过 IK 分词器进行词频统计。使用分词器对文章的词频进行统计&#xff0c;主要目的是实现如下图所示的词云功能&#xff0c;可以找到文章内的重点词汇。后续也可以对词进行词性标注&#xff0c;实体识别以及对…

IC Nansha|AMD高级副总裁、大中华区总裁潘晓明:制程、架构、平台优化突破计算边界

6月25日&#xff0c;中国南沙国际集成电路产业论坛在广州南沙顺利举行。AMD高级副总裁、大中华区总裁潘晓明出席了本次会议&#xff0c;并在高峰论坛环节中以《高性能计算的未来》为主题发表了演讲。 &#xff08;AMD高级副总裁、大中华区总裁 潘晓明&#xff09; 作为一家深耕…

爱数SMART 2022峰会开启,分享数据战略与建设数据驱动型组织方法论

6月28日&#xff0c;爱数SMART 2022线上峰会全球直播正式开启。主论坛上&#xff0c;爱数正式提出了企业制定数据战略以及建设数据驱动型组织的方法论&#xff0c;并推出开源计划与数字伙伴计划2.0&#xff0c;共创数据驱动型组织。 通过清晰的数据战略&#xff0c;从容加速数据…

云原生时代开发者工具变革探索与实践

简介&#xff1a;本篇内容分享了原生时代开发者工具变革探索与实践。 分享人&#xff1a;马洪喜 行云创新CEO 正文&#xff1a;本篇内容将通过三个部分来介绍云原生时代开发者工具变革探索与实践。 一、云原生模块化开发概览 二、软件模块化开发特点 三、ADD产品简介 一、…

喜马拉雅 Apache RocketMQ 消息治理实践

简介&#xff1a;本文通过喜马拉雅的RocketMQ治理实践分享&#xff0c;让大家了解使用消息中间件过程中可能遇到的问题&#xff0c;避免实战中踩坑。 作者&#xff1a;曹融&#xff0c;来自喜马拉雅&#xff0c;从事微服务和消息相关中间件开发。 本文通过喜马拉雅的RocketMQ治…

Docker 容器为什么傲娇?全靠镜像撑腰!

作者 | 飞向星的客机来源 | CSDN博客&#x1f31f; 前言Docker 镜像是 Docker 容器的基石&#xff0c;容器是镜像的运行实例&#xff0c;有了镜像才能启动容器。Docker 镜像是一个只读的模板&#xff0c;一个独立的文件系统&#xff0c;包括运行一个容器所需的数据&#xff0c;…

HBase读链路分析

简介&#xff1a;HBase的存储引擎是基于LSM-Like树实现的&#xff0c;更新操作不会直接去更新数据&#xff0c;而是使用各种type字段&#xff08;put&#xff0c;delete&#xff09;来标记一个新的多版本数据&#xff0c;采用定期compaction的形式来归档合并数据。这种数据结构…

PolarDB for PostgreSQL 开源路线图

简介&#xff1a;作者&#xff1a;蔡乐 本文主要分享一下Polar DB for PG的开源路线图&#xff0c;虽然路线图已经拟定&#xff0c;但是作为开源产品&#xff0c;所有参与者都能提出修改意见&#xff0c;包括架构核心特性的技术以及周边生态和工具等&#xff0c;希望大家能够踊…