Avatar

Profiling C++ code with Frida (2nd part)

ionicons-v5-kRomain Thomas April 8, 2021
Wave

Tl;DR

This blog post is not, strictly speaking, related to LIEF but it aims at completing the previous blog about profiling code with Frida. In particular, it exposes the limits of our approach regarding the Microsoft/Itanium ABI.

Long story short, the previous code does not work on Linux/OSX for virtual functions.

The previous blog post tried to show a use case of Frida to profile C++ functions. In particular, it exposed what we called a trick to convert a C++ member function into a void*:

1template<typename Func>
2inline void* cast_func(Func f) {
3  union {
4    Func func;
5    void* p;
6  };
7  func = f;
8  return p;
9}

First, and as noticed by Julien Jorge, writing a union’s field and accessing another field of this union is undefined behavior:

It’s undefined behavior to read from the member of the union that wasn’t most recently written. Many compilers implement, as a non-standard language extension, the ability to read inactive members of a union.

Thanks also to the feedback from Julien Jorge, there is another issue when converting a C++ member function into a raw pointer.

Basically, a member function pointer is not the same kind of pointer as a regular C function. While the regular size of a C function pointer is the same as sizeof(void*), the size of a member function pointer is usually greater:

1struct Foo {
2  void bar() {}
3};
4
5int main() {
6  printf("sizeof(&Foo::bar): %d\n", sizeof(&Foo::bar));
7  return 0;
8}
1$ clang++ sizeof_member.cpp -o sizof_member
2$ ./sizeof_member
3sizeof(&Foo::bar): 16

The layout of a member function pointer is ABI specific but according to LLVM’s source code we can distinguish two ABI that describe this layout:

  1. Itanium CXX ABI which is used on Linux, iOS, OSX, Android, …
  2. Microsoft

Itanium ABI

For the Itanium CXX ABI and according to the official documentation, non-virtual functions have the following structure:

1struct {
2  uintptr_t ptr;
3  ptrdiff_t adj;
4};

Where, ptr is the address of the function and adj is an offset applied on this in the case of multi-inheritance.

So in our bad-coded casting function cast_func(), it works as expected for non-virtual functions since we access the first field ptr which is the function pointer. We can observe these two fields with the following piece of code 1:

 1template<typename Func>
 2void print(Func f) {
 3  union {
 4    Func fcn;
 5    struct {
 6      uintptr_t ptr;
 7      ptrdiff_t adj;
 8    };
 9  };
10  fcn = f;
11  printf("%016lx | %016lx\n", ptr, adj);
12}

that outputs this kind of values:

1struct Foo {
2  void bar() {}
3};
4
5int main() {
6  print(&Foo::bar);
7  return 0;
8}
$ ./show_fields
00005568e19021e0 | 0000000000000000

If bar() were a virtual function, the meaning of the ptr field would be different. Still according to the Itanium CXX ABI, the value of ptr in the case of a virtual function is 1 plus the offset of the function within the v-table. In particular, we can’t access the address of the function without this since the vtable is embedded in the layout of the object. 2

Itanium CXX ABI

Microsoft ABI

Regarding the Microsoft ABI, there is not as much documentation compared to the “Linux/OSX” ABI. LLVM supports this ABI as described in clang/lib/CodeGen/MicrosoftCXXABI.cpp but I was still curious to know how (without LLVM) the layout of a function member pointer looks like. One could look at c1xx.dll/c2.dll located in the Visual Studio directory but these libraries are not straightforward to reverse.

Alternately, we can try to infer the layout from the assembly code output. First of all, the result of sizeof() applied to a function member pointer is 16. 16 being twice a pointer’s size on an 64-bits architecture, we can start following the Itanium ABI and confirm or infirm our choices:

1struct FuncMemPtr {
2  uintptr_t unknown1;
3  uintptr_t unknown2;
4};

Then we can unpack the fields of the function member pointer with the union trick:

 1struct Base1 {
 2  virtual void f() { }
 3};
 4
 5struct Base2 {
 6  virtual void g() {}
 7};
 8
 9struct Derived2 : Base2, Base1 {
10  virtual void f() {}
11  virtual void g() {}
12  virtual h() {}
13};
14
15template<typename Func>
16void info(Func f) {
17  union {
18    Func fcn;
19    struct {
20      uintptr_t unknown1;
21      uintptr_t unknown2;
22    };
23  };
24  fcn = f;
25}
26
27int main() {
28  info(&Derived2::h);
29  info(&Derived2::f);
30  return 0;
31}

The layout of the non-virtual function Derived2::h() seems to follow the same layout as the Itanium ABI where we find the function pointer in the first field.

Non virtual function layout

For the virtual function Derived2::f, we can notice a first memory write that fills the first field with a pointer to a thunk 3 function while the second field contains a constant which matches the value of this adjustor. For the second field (this adjustor), we can switch from &Derived2::f to &Derived2::g to confirm that it changes accordingly to the output of /d1reportAllClassLayout

Virtual function layout

This leads to the following guessing:

1struct MsvcCXXFuncMember {
2  uintptr_t fnc_ptr; // That can be a thunk for virtual function
3  int adjustor; // int because of mov DWORD and not mov QWORD in this assembly output
4};

These two fields follow the LLVM implementation:

 1struct {
 2  // A pointer to the member function to call.  If the member function is
 3  // virtual, this will be a thunk that forwards to the appropriate vftable
 4  // slot.
 5  void *FunctionPointerOrVirtualThunk;
 6
 7  // An offset to add to the address of the vbtable pointer after
 8  // (possibly) selecting the virtual base but before resolving and calling
 9  // the function.
10  // Only needed if the class has any virtual bases or bases at a non-zero
11  // offset.
12  int NonVirtualBaseAdjustment;
13
14  // The offset of the vb-table pointer within the object. Only needed for
15  // incomplete types.
16  int VBPtrOffset;
17
18  // An offset within the vb-table that selects the virtual base containing
19  // the member.  Loading from this offset produces a new offset that is
20  // added to the address of the vb-table pointer to produce the base.
21  int VirtualBaseAdjustmentOffset;
22};

From LLVM, we also learn that the full layout can contain up to four fields. We can trigger the third field with the following change:

1@@ -11,3 +11,3 @@
2-struct Derived2 : Base2, Base1 {
3+struct Derived2 : Base2, virtual Base1 {
4   virtual void f() {}
5@@ -32 +32,2 @@
6 }

VBPtrOffset

The fourth field is a bit more tricky to trigger and the following code comes from the LLVM test suite 4

 1struct B1 {
 2  void foo();
 3  int b;
 4};
 5struct B2 {
 6  int b2;
 7  int v;
 8  void foo();
 9};
10
11struct UnspecWithVBPtr;
12int UnspecWithVBPtr::*forceUnspecWithVBPtr;
13struct UnspecWithVBPtr : B1, virtual B2 {
14  int u;
15  void foo();
16};

VBPtrOffset

We can notice that the result of sizeof() applied to UnspecWithVBPtr::foo is 24: sizeof(uintptr_t) + 3 * sizeof(int) + padding

Conclusion

The profiler described in the first blog post works as expected for non-virtual but does not work with virtual functions that follow the Itanium ABI. To work with virtual functions, we would need to pass an extra parameter to the object that implements the virtual functions. By assuming that the vtable is placed at the beginning of the object’s layout, we can support such functions with the following modifications:

 1diff --git a/main.cpp b/main.cpp
 2index d30a0c1..65d18eb 100644
 3--- a/main.cpp
 4+++ b/main.cpp
 5
 6+struct Foo {
 7+  virtual void bar() {
 8+    std::cout << "In bar" << std::endl;
 9+  }
10+  uint8_t x = 1;
11+};
12+
13
14@@ -88,9 +95,10 @@ struct Profiler {
15
16-  void setup() {
17-    PROFILE(LIEF::ELF::Parser::init);
18-    PROFILE(LIEF::ELF::Parser::parse_segments<LIEF::ELF::ELF64>);
19+  template<class T>
20+  void setup(const T& obj) {
21+    const uintptr_t vtable = *reinterpret_cast<const uintptr_t*>(&obj);
22+    profile_func(&Foo::bar, "Foo:bar", vtable);
23   }
24
25@@ -98,8 +106,13 @@ struct Profiler {
26   template<typename Func>
27-  void profile_func(Func func, std::string name) {
28+  void profile_func(Func func, std::string name, uintptr_t vtable = 0) {
29     void* addr = cast_func(func);
30+
31+    if (vtable > 0) {
32+      const uintptr_t voff = reinterpret_cast<uintptr_t>(addr) - 1;
33+      addr = *reinterpret_cast<void**>(vtable + voff);
34+    }
35     funcs[reinterpret_cast<uintptr_t>(addr)] = std::move(name);
36     gum_interceptor_begin_transaction (ctx_->interceptor);
37     gum_interceptor_attach (ctx_->interceptor,
38@@ -130,8 +143,9 @@ int main(int argc, const char** argv) {
39     return 1;
40   }
41
42+  Foo f;
43   Profiler& prof = Profiler::get();
44-  prof.setup();
45-  LIEF::ELF::Parser::parse(argv[1]);
46+  prof.setup(f);
47+  f.bar();
48   return 0;
49 }

The Microsoft C++ ABI is poorly documented but the LLVM project is a good reference for that. One might also be interested in this presentation (Bringing Clang and LLVM to Visual C++ users) that outlines the challenges for LLVM developers to support this ABI.

Acknowledgment

Thanks to Julien Jorge for proofreading this post and his valuable feedback.


  1. Which is still UB ↩︎

  2. There is an exception for the ARM architecture:

    In the 32-bit ARM representation, the this-adjustment stored in adj is left-shifted by one, and the low bit of adj indicates whether ptr is a function pointer (including null) or the offset of a v-table entry. A virtual member function pointer sets ptr to the v-table entry offset as if by reinterpret_cast<fnptr_t>(uintfnptr_t(offset)). A null member function pointer sets ptr to a null function pointer and must ensure that the low bit of adj is clear; the upper bits of adj remain unspecified.

    ↩︎
  3. A thunk function is generated by the compiler as a trampoline to the right virtual function. This trampoline can also be used to fix this pointer with the given adjustor. ↩︎

  4. The layout of this code goes beyond my understanding ↩︎

Avatar
Romain Thomas Posted on April 8, 2021