#669416 ltrace does not report virtual methods from heap-allocated objects

Package:
ltrace
Source:
ltrace
Description:
Tracks runtime library calls in dynamically linked programs
Submitter:
Chip Christian
Date:
2024-11-30 01:21:02 UTC
Severity:
normal
Tags:
#669416#5
Date:
2012-04-19 17:36:52 UTC
From:
To:
using g++ 4.4.6, ltrace 0.6.0 and earlier

I built a shared library from foo.cpp and linked it to a program built
from f.cpp.  All method names are reported for an auto, but if I call
operator new, the virtual (pure or otherwise) never show up:

$ ltrace -C ./f
(0, 0, 0x219800, -1, 0x1f25bc2)                  = 0x3715a21160
__libc_start_main(0x4008e4, 1, 0x7fff51839b48, 0x400a40, 0x400a30
<unfinished ...>
foo::static_foo()(1, 0x7fff51839b48, 0x7fff51839b58, 4, 0x3715f8b300)
= 0x3715f8cf60
operator new(unsigned long)(8, 0x7fff51839b48, 0x7fff51839b58, 4,
0x3715f8b300) = 0x1186010
foo::real_foo()(0x1186010, 0x1186020, 33, 0, 135168) = 0x1186010
foo::real_foo()(0x7fff51839a40, 0x1186020, 0x7f4dad83da86, 0, 135168)
= 0x7fff51839a40
foo::virtual_foo()(0x7fff51839a40, 0x1186020, 0x7f4dad83da86, 0,
135168) = 0x7fff51839a40
foo_child::pure_virtual_foo()(0x7fff51839a40, 0x1186020,
0x7f4dad83da86, 0, 135168) = 0x7fff51839a40
+++ exited (status 0) +++

foo.hpp:
class foo
{
    public:
    foo(){};
    ~foo(){};
    static void static_foo();
    virtual void virtual_foo();
    virtual void pure_virtual_foo() = 0;
    void real_foo();
};

class foo_child: public foo
{
    public:
    void pure_virtual_foo();
};

foo.cpp:
#include "foo.hpp"
#include <malloc.h>

void foo::static_foo()
{
}

void foo::virtual_foo()
{
}

void foo::real_foo()
{
}

void foo_child::pure_virtual_foo()
{
}


f.cpp:
#include "foo.hpp"

int main()
{
    foo::static_foo();

    foo_child* f = new foo_child;
    f->real_foo();
    f->virtual_foo();
    // ltrace doesn't report this one?
    f->pure_virtual_foo();

    foo_child g;
    g.real_foo();
    g.virtual_foo();

    // ltrace doe report this one!
    g.pure_virtual_foo();

    return 0;
}

Makefile for completeness:

all: f

f: f.cpp libfoo.so
        g++ -Wall -L. -lfoo f.cpp -o f

libfoo.so: foo.cpp foo.hpp
        g++ -Wall -fPIC -shared -o libfoo.so foo.cpp

clean:
        rm -f f *.o *.so

#669416#10
Date:
2024-11-30 01:17:02 UTC
From:
To:
Yes.
If we reduce f.cpp to
  #include "foo.hpp"
  int main() {
      foo* f = new foo_child;
      // ltrace doesn't report this one?
      f->virtual_foo();
      f->pure_virtual_foo();
  }
then
  $ nm -C f | grep foo
  0000000000001190 W foo::foo()
  0000000000001190 W foo::foo()
  00000000000011aa W foo_child::foo_child()
  00000000000011aa W foo_child::foo_child()
  0000000000003da0 V vtable for foo
  0000000000003d80 V vtable for foo_child

Because f is /actually/ fully virtual,
so calling either function actually looks like
  (f->_vtable[1])(f)
and the initialiser for f looks like
  f = malloc(...);
  memcpy(f._vtable, vtable for foo_child)

Whereas real
  #include "foo.hpp"
  int main() {
      foo_child g;
      // ltrace does report this one
      g.virtual_foo();
      g.pure_virtual_foo();
  }
yields
  $ nm -C f | grep foo
                   U foo::virtual_foo()
  00000000000011ba W foo::foo()
  00000000000011ba W foo::foo()
  00000000000011d4 W foo::~foo()
  00000000000011d4 W foo::~foo()
                   U foo_child::pure_virtual_foo()
  00000000000011ee W foo_child::foo_child()
  00000000000011ee W foo_child::foo_child()
  0000000000001218 W foo_child::~foo_child()
  0000000000001218 W foo_child::~foo_child()
  0000000000003d90 V vtable for foo
  0000000000003d70 V vtable for foo_child
because g is a concrete type so it's devirtualised,
so the calls /are/ actually
  foo_child::virtual_foo(&g)
&c.

You'd also see this from stack-allocated variables in more complex
scenarios that the compiler can't solve and devirtualise.

I'm pretty sure this is definitionally unsolvable.

#669416#19
Date:
2024-11-30 01:17:02 UTC
From:
To:
Yes.
If we reduce f.cpp to
  #include "foo.hpp"
  int main() {
      foo* f = new foo_child;
      // ltrace doesn't report this one?
      f->virtual_foo();
      f->pure_virtual_foo();
  }
then
  $ nm -C f | grep foo
  0000000000001190 W foo::foo()
  0000000000001190 W foo::foo()
  00000000000011aa W foo_child::foo_child()
  00000000000011aa W foo_child::foo_child()
  0000000000003da0 V vtable for foo
  0000000000003d80 V vtable for foo_child

Because f is /actually/ fully virtual,
so calling either function actually looks like
  (f->_vtable[1])(f)
and the initialiser for f looks like
  f = malloc(...);
  memcpy(f._vtable, vtable for foo_child)

Whereas real
  #include "foo.hpp"
  int main() {
      foo_child g;
      // ltrace does report this one
      g.virtual_foo();
      g.pure_virtual_foo();
  }
yields
  $ nm -C f | grep foo
                   U foo::virtual_foo()
  00000000000011ba W foo::foo()
  00000000000011ba W foo::foo()
  00000000000011d4 W foo::~foo()
  00000000000011d4 W foo::~foo()
                   U foo_child::pure_virtual_foo()
  00000000000011ee W foo_child::foo_child()
  00000000000011ee W foo_child::foo_child()
  0000000000001218 W foo_child::~foo_child()
  0000000000001218 W foo_child::~foo_child()
  0000000000003d90 V vtable for foo
  0000000000003d70 V vtable for foo_child
because g is a concrete type so it's devirtualised,
so the calls /are/ actually
  foo_child::virtual_foo(&g)
&c.

You'd also see this from stack-allocated variables in more complex
scenarios that the compiler can't solve and devirtualise.

I'm pretty sure this is definitionally unsolvable.