c++ - How can I omit unused virtual functions when linking? - Stack Overflow

I use C++ with arm-none-eabi-gcc compiler to write software for a MCU.For this I use a wrapper which c

I use C++ with arm-none-eabi-gcc compiler to write software for a MCU. For this I use a wrapper which can use different implementations. The issue is when one of the functions of the wrapper is used, all the functions (even they are not called) are in the compiled binary including the huge library code they use which takes a lot of unnecessary space.

I tried to create a minimal example to illustrate the issue:

Wrapper.h:

#include <stdint.h>
#include "ClassA.h"

class Wrapper {
 public:
  static float square(float x);
  static float reciprocal(float x);

  static void setImplementation(ClassA *impl);

 private:
  static ClassA *classA;
};

Wrapper.cpp:

#include "Wrapper.h"

ClassA *Wrapper::classA = nullptr;

float Wrapper::square(float x) { return classA->square(x); }
float Wrapper::reciprocal(float x) { return classA->reciprocal(x); }
void Wrapper::setImplementation(ClassA *impl) { classA = impl; }

ClassA.h:

#include <stdint.h>

class ClassA {
 public:
  virtual ~ClassA() = default;
  virtual float square(float x);
  virtual float reciprocal(float x);
};

ClassA.cpp:

#include "ClassA.h"

float ClassA::square(float x) {
  float y = x * x;
  return y;
}

float ClassA::reciprocal(float x) {
  float y = 1.0f / x;
  return y;
}

When I then call one of the function via the wrapper

ClassA classA;
Wrapper::setImplementation(&classA);
static volatile float value = 0.6f;
static volatile float test = Wrapper::reciprocal(value);

it seems that always all the functions of ClassA (in this case two) are in binary, even they are not called. Generally removing unused function from the binary works, only when using this construct, it does not.

Any idea why all the functions are in the binary and how I could I avoid it?

Thanks in advance!

Regards, Martin

update: The compile and link command which I use for this example is:

arm-none-eabi-g++ -mcpu=cortex-m0plus -march=armv6-m -mthumb -mlittle-endian -mfloat-abi=soft \
                  -ggdb -Os -flto=auto -ffunction-sections -fdata-sections -fsingle-precision-constant \
                  -fno-rtti -fno-exceptions -Wno-register \
                  -Wno-pedantic -Wno-unused-parameter -Wall -Wextra -Wpedantic -Wdouble-promotion \
                  -DARM_MATH_CM0 -DCBOR_NO_HALF_FLOAT_TYPE -DDEBUG \
                  -nostartfiles --specs=nano.specs --specs=nosys.specs -Wl,--gc-sections,-no-wchar-size-warning \
                  -Wl,-e,main,-Map=output.map -T/workspaces/mspm0g1519.lds \
                  -I/workspaces/src/sandbox \
                  -o output.elf \
                  /workspaces/src/main.cpp \
                  /workspaces/src/sandbox/ClassA.cpp \
                  /workspaces/src/sandbox/Wrapper.cpp

I use C++ with arm-none-eabi-gcc compiler to write software for a MCU. For this I use a wrapper which can use different implementations. The issue is when one of the functions of the wrapper is used, all the functions (even they are not called) are in the compiled binary including the huge library code they use which takes a lot of unnecessary space.

I tried to create a minimal example to illustrate the issue:

Wrapper.h:

#include <stdint.h>
#include "ClassA.h"

class Wrapper {
 public:
  static float square(float x);
  static float reciprocal(float x);

  static void setImplementation(ClassA *impl);

 private:
  static ClassA *classA;
};

Wrapper.cpp:

#include "Wrapper.h"

ClassA *Wrapper::classA = nullptr;

float Wrapper::square(float x) { return classA->square(x); }
float Wrapper::reciprocal(float x) { return classA->reciprocal(x); }
void Wrapper::setImplementation(ClassA *impl) { classA = impl; }

ClassA.h:

#include <stdint.h>

class ClassA {
 public:
  virtual ~ClassA() = default;
  virtual float square(float x);
  virtual float reciprocal(float x);
};

ClassA.cpp:

#include "ClassA.h"

float ClassA::square(float x) {
  float y = x * x;
  return y;
}

float ClassA::reciprocal(float x) {
  float y = 1.0f / x;
  return y;
}

When I then call one of the function via the wrapper

ClassA classA;
Wrapper::setImplementation(&classA);
static volatile float value = 0.6f;
static volatile float test = Wrapper::reciprocal(value);

it seems that always all the functions of ClassA (in this case two) are in binary, even they are not called. Generally removing unused function from the binary works, only when using this construct, it does not.

Any idea why all the functions are in the binary and how I could I avoid it?

Thanks in advance!

Regards, Martin

update: The compile and link command which I use for this example is:

arm-none-eabi-g++ -mcpu=cortex-m0plus -march=armv6-m -mthumb -mlittle-endian -mfloat-abi=soft \
                  -ggdb -Os -flto=auto -ffunction-sections -fdata-sections -fsingle-precision-constant \
                  -fno-rtti -fno-exceptions -Wno-register \
                  -Wno-pedantic -Wno-unused-parameter -Wall -Wextra -Wpedantic -Wdouble-promotion \
                  -DARM_MATH_CM0 -DCBOR_NO_HALF_FLOAT_TYPE -DDEBUG \
                  -nostartfiles --specs=nano.specs --specs=nosys.specs -Wl,--gc-sections,-no-wchar-size-warning \
                  -Wl,-e,main,-Map=output.map -T/workspaces/mspm0g1519.lds \
                  -I/workspaces/src/sandbox \
                  -o output.elf \
                  /workspaces/src/main.cpp \
                  /workspaces/src/sandbox/ClassA.cpp \
                  /workspaces/src/sandbox/Wrapper.cpp
Share Improve this question edited Apr 8 at 12:18 Toby Speight 31.4k52 gold badges76 silver badges113 bronze badges asked Mar 13 at 16:44 MartinMartin 312 bronze badges 10
  • 3 How do you build? What are your actual and exact commands to compile and link? – Some programmer dude Commented Mar 13 at 16:49
  • Related to how-to-remove-unused-c-c-symbols-with-gcc-and-ld – Jarod42 Commented Mar 13 at 16:49
  • 2 It's been years since I looked into this, but at the time linkers could take an entire object file or none of it. Assuming that's still true, you'd have to put each function in a different object file. – Stephen Newell Commented Mar 13 at 16:59
  • 1 @StephenNewell: or tell to the compiler to split TU in different sections. – Jarod42 Commented Mar 13 at 17:04
  • 1 Try -ffunction-sections – Ben Voigt Commented Mar 13 at 17:33
 |  Show 5 more comments

2 Answers 2

Reset to default 3

Any idea why all the functions are in the binary and how could I avoid it?

Why are all the functions in the binary?

Let's see. This is your example code brushed up:

$ tail -n +1 *.h *.cpp
==> ClassA.h <==
#ifndef CLASSA_H
#define CLASSA_H

class ClassA {
public:
    virtual ~ClassA() = default;
    virtual float square(float x);
    virtual float reciprocal(float x);
};

#endif

==> Wrapper.h <==
#ifndef WRAPPER_H
#define WRAPPER_H

#include "ClassA.h"

class Wrapper {
public:
    static float square(float x);
    static float reciprocal(float x);

    static void setImplementation(ClassA *impl);

private:
    static ClassA *classA;
};

#endif

==> ClassA.cpp <==
#include "ClassA.h"

float ClassA::square(float x) {
  float y = x * x;
  return y;
}

float ClassA::reciprocal(float x) {
  float y = 1.0f / x;
  return y;
}

==> main.cpp <==
#include <iostream>
#include "Wrapper.h"

int main() {
    ClassA classA;
    Wrapper::setImplementation(&classA);
    static volatile float value = 0.6f;
    static volatile float test = Wrapper::reciprocal(value);
    std::cout << test << std::endl;
}

==> Wrapper.cpp <==
#include "Wrapper.h"

ClassA *Wrapper::classA = nullptr;

float Wrapper::square(float x) { return classA->square(x); }
float Wrapper::reciprocal(float x) { return classA->reciprocal(x); }
void Wrapper::setImplementation(ClassA *impl) { classA = impl; }

Compile and link, discarding the dead wood:

$ arm-none-eabi-g++ -c *.cpp -Os -Wall -Wextra -pedantic -fno-rtti -ffunction-sections -fdata-sections -save-temps
$ arm-none-eabi-g++ main.o Wrapper.o ClassA.o -Wl,-gc-sections --specs=rdimon.specs

I saved the intermediate files (-save-temps) because I'll want some of the assembly.

The program runs (in emulator):

$ qemu-arm a.out
1.66667

OK, that's reciprocal 0.6.

Let's see what ClassA things are in the symbol table:

$ readelf --symbols --wide --demangle a.out | grep ClassA
  2274: 00000000     0 FILE    LOCAL  DEFAULT  ABS ClassA.cpp
  5115: 0006bb48    24 OBJECT  GLOBAL DEFAULT    4 vtable for ClassA
  5120: 000090c4    28 FUNC    WEAK   DEFAULT    2 ClassA::~ClassA()
  5400: 000090c0     4 FUNC    WEAK   DEFAULT    2 ClassA::~ClassA()
  5494: 000090c0     4 FUNC    WEAK   DEFAULT    2 ClassA::~ClassA()
  5836: 00009098    20 FUNC    GLOBAL DEFAULT    2 ClassA::square(float)
  5858: 000090ac    20 FUNC    GLOBAL DEFAULT    2 ClassA::reciprocal(float)
  6259: 00009088    16 FUNC    GLOBAL DEFAULT    2 Wrapper::setImplementation(ClassA*)
    

Besides the referenced member:

5858: 000090ac    20 FUNC    GLOBAL DEFAULT    2 ClassA::reciprocal(float)
    

and the unreferenced:

5836: 00009098    20 FUNC    GLOBAL DEFAULT    2 ClassA::square(float)
    

that we don't want, there is:

5115: 0006bb48    24 OBJECT  GLOBAL DEFAULT    4 vtable for ClassA

the vtable of ClassA. A polymorphic class's vtable is an implementational entity (that has no recognition in the C++ Standard). It is (mostly) an array of pointers to ClassA's virtual function members needed for polymorphic function despatch. We know that &ClassA::square and &ClassA::reciprocal are in that list and can observe them there in the assembly of ClassA.cpp. The mangled name of vtable for ClassA is:

$ readelf --symbols --wide a.out | grep 'OBJECT.*ClassA'
  5115: 0006bb48    24 OBJECT  GLOBAL DEFAULT    4 _ZTV6ClassA
    

and here's its definition from ClassA.s, which we have from -save-temps:

    .global _ZTV6ClassA
    .section    .rodata._ZTV6ClassA,"a"
    .align  2
    .type   _ZTV6ClassA, %object
    .size   _ZTV6ClassA, 24
_ZTV6ClassA:
    .word   0
    .word   0
    .word   _ZN6ClassAD1Ev
    .word   _ZN6ClassAD0Ev
    .word   _ZN6ClassA6squareEf
    .word   _ZN6ClassA10reciprocalEf
        

It's an array of 6 32-bit quantities of which the last 4 are the member function addresses:

  • _ZN6ClassAD1Ev: ClassA::~ClassA(), complete object destructor
  • _ZN6ClassAD0Ev: ClassA::~ClassA(), deleting destructor
  • _ZN6ClassA6squareEf : ClassA::square(float)
  • _ZN6ClassA10reciprocalEf: ClassA::reciprocal(float)

(The first null member is the offset-to-top value, which would be non-null if ClassA were a multiply derived class. The second null member is the typeinfo pointer, which would be non-null if we hadn't disabled RTTI.ClassA's third destructor, which you maybe noted in the demangled readelf output, is _ZN6ClassAD2Ev, and is its non-virtual base-object destructor: not in the vtable.)

So &ClassA::square is in the image because it is referenced from vtable for ClassA. And vtable for ClassA is in the image because the program makes a virtual function call through it within:

static volatile float test = Wrapper::reciprocal(value);

per the definition:

float Wrapper::reciprocal(float x) { return classA->reciprocal(x); }

It doesn't matter that the program does not call ClassA::square. You call through ClassA's vtable, the linker links the vtable - a data-section containing it is a minimal unit of linkage - and it has to link anything referenced through the vtable, on pain of emitting a dangling pointer.


How could I avoid it?

GCC has no C++ feature that enables the linker to identity and nullify vtable entries that reference unused virtual functions - which would enable the linker to garbage-collect function-sections that define unused virtual functions. Neither has Clang. Way back at GCC 3.1, g++ for a while had the option fvtable-gc for this purpose:

-fvtable-gc

Emit special relocations for vtables and virtual function references so that the linker can identify unused virtual functions and zero out vtable slots that refer to them. This is most useful with -ffunction-sections and -Wl,--gc-sections, in order to also discard the functions themselves.

This optimization requires GNU as and GNU ld. Not all systems support this option. -Wl,--gc-sections is ignored without -static.

But the feature was yanked by 3.4.6 at latest. You can glean from the language that it was a difficult feature. (Although as you commented, the ARM Realview toolchain apparently manages it).


That leaves you with the option of hacking: intervene between compilation and linkage do what -fvtable-gc was meant to enable and zero out the vtable slots for virtual functions you know can't be called in your program.

Is the image space that you will save worth going off-piste for? You'd best consult on that if you're not you're own boss on this.

The actual mechanics of zeroing out vtable slots are simple, in assembly. In general, the hard bit is deducing which ones are unreachable. But if we take your example as valid, then you just have to assemble another definition of _ZTV6ClassA that nullifies the reference to ClassA::square(float), i.e.:

    .global _ZTV6ClassA
    .section    .rodata._ZTV6ClassA,"a"
    .align  2
    .type   _ZTV6ClassA, %object
    .size   _ZTV6ClassA, 24
_ZTV6ClassA:
    .word   0
    .word   0
    .word   _ZN6ClassAD1Ev
    .word   _ZN6ClassAD0Ev
    .word   0
    .word   _ZN6ClassA10reciprocalEf
    

and input it to your linkage instead of the one linked from ClassA.o. With -ffunction-sections -fdata-sections at compilation and -Wl,-gc-sections at linkage, the input vtable data section .rodata._ZTV6ClassA will no longer reference _ZN6ClassA6squareEf, and nothing else does, so the linker will be able to discard the function section .text._ZN6ClassA6squareEf that defines it.


For your example case, we could simply edit the assembly file ClassA.s to replace:

.word   _ZN6ClassA6squareEf

with:

.word   0

Then assemble the edited version as, say, ClassA_hacked.o, and link it in the program instead of ClassA.o

But it would be useful to have some general automation for pruning a vtable in this way. Here's a bash script to that end:

$ cat prune_vtable.sh
#!/usr/bin/bash

script=$(basename $0)
delete_syms=()
vtable_sym=''
    usage() {
        echo "$script: Prune unused virtual methods from a class vtable. "
        echo "  Read GAS_ASSEMBLY_FILE; output to stdout a revision in which "
        echo "  the DELETE_SYMBOL slots are zeroed out in the vtable with name VTABLE_SYMBOL."   
        echo "Usage: $script) -h | -v VTABLE_SYMBOL -s DELETE_SYMBOL [ -s DELETE_SYMBOL] GAS_ASSEMBLY_FILE "
        echo "  -h: Print this help and exit"
        echo "  -v: VTABLE_SYMBOL is the vtable symbol from which to delete member function pointers"
        echo "  -s: DELETE_SYMBOL is a virtual member function symbol to delete from the vtable"
        echo "      May be given multiple times"
        echo "  GAS_ASSEMBLY_FILE is the assembly file in which VTABLE_SYMBOL is defined"
    }

while getopts "v:s:h" o; do
    case "${o}" in
        v)
            vtable_sym=${OPTARG}
            ;;
        s)
            delete_syms+=(${OPTARG})
            ;;
        h)
            usage;
            exit 0
            ;;
        *)
            usage;
            exit 1
            ;;
    esac
done
assembly_file=${@:$OPTIND:1}

if [[ -z "$assembly_file" || -z "$vtable_sym" || ${#delete_syms[@]} == 0 ]]
then
    usage
    exit 1
fi

((state=0))
tab=$'\t'
wordtype=''
((wordvaloff=0))
deleted_syms=()
    
echo "/*    This file output by $script from input $assembly_file "
echo "  to remove presumptively unused symbol(s): "
printf "\t\t%s\n" ${delete_syms[*]}
echo "  from vtable $vtable_sym."
echo "*/"  
while IFS= read -r line; do
    if [[ $state == 0 ]]; then
        printf "%s\n" "$line"
        if [[ "$line" =~ ^${tab}\.(global|globl|weak)${tab}${vtable_sym}$ ]]; then
            ((state++))
        fi
        continue
    fi
    if [[ $state == 1 ]]; then
        printf "%s\n" "$line"
        if [[ "$line" == "$vtable_sym:" ]]; then
            ((state++))
        fi
        continue
    fi
    if [[ $state == 2 ]]; then
        if [[ -z "$wordtype" ]]; then
            if [[ "${line:0:7}" == "${tab}.quad${tab}" ]]; then # x86_64
                ((wordvaloff=7))
                wordtype=".quad"
            elif [[ "${line:0:7}" == "${tab}.long${tab}" ]]; then # x86_32
                ((wordvaloff=7))
                wordtype=".long"
            elif [[ "${line:0:7}" == "${tab}.word${tab}" ]]; then # arm32/riscv32
                ((wordvaloff=7))
                wordtype=".word"
            elif [[ "${line:0:8}" == "${tab}.xword${tab}" ]]; then # arm64
                ((wordvaloff=8))
                wordtype=".xword"
            elif [[ "${line:0:8}" == "${tab}.dword${tab}" ]]; then # riscv64 
                ((wordvaloff=8))
                wordtype=".dword"
            fi
        fi
        if [[ "${line:0:$wordvaloff}" == "${tab}${wordtype}${tab}" ]]; then
            wordval="${line:$wordvaloff}"
            if [[ " ${delete_syms[*]} " =~ [[:space:]]${wordval}[[:space:]] ]] then
                printf "/* Nullified next by $script: $line */\n"
                printf "\t%s\t0\n" "${wordtype}"
                delete_syms=(${delete_syms[@]/$wordval})
                deleted_syms+=($wordval)
                continue
            fi
            printf "%s\n" "$line"
            continue
        fi
        printf "%s\n" "$line"
        ((state++))
        continue
    fi
    if [[ $state == 3 ]]; then printf "%s\n" "$line"; fi
done < $assembly_file
if [[ $state == 0 ]]; then
    printf "*** ERROR: -v symbol %s was not located in file %s\n" \
        $vtable_sym $assembly_file >&2
    exit 1
fi
if [[ ${#delete_syms[@]} > 0 ]]; then
    printf \
        "*** ERROR: The following -s symbols were not found in vtable %s " \
        $vtable_sym >&2
    printf "in file %s:\n" $assembly_file >&2
    printf "\t%s\n"  ${delete_syms[*]} >&2
    exit 1
fi
printf "\t%s\t%s" ".ident" "\"Symbols ["
printf " %s " ${deleted_syms[*]} 
printf "] were nullified in vtable %s by %s\"\n" $vtable_sym $script
exit 0

We can use prune_vtable.sh in your example case like this, with the assembly source ClassA.s

$ ./prune_vtable.sh -v _ZTV6ClassA -s _ZN6ClassA6squareEf ClassA.s > ClassA_hacked.s

which gives us the diffs:

$ diff ClassA.s ClassA_hacked.s 
0a1,5
> /*    This file output by prune_vtable.sh from input ClassA.s 
>   to remove presumptively unused symbol(s): 
>       _ZN6ClassA6squareEf
>   from vtable _ZTV6ClassA.
> */
108c113,114
<   .word   _ZN6ClassA6squareEf
---
> /* Nullified next by prune_vtable.sh:     .word   _ZN6ClassA6squareEf */
>   .word   0
110a117
>   .ident  "Symbols [ _ZN6ClassA6squareEf ] were nullified in vtable _ZTV6ClassA by prune_vtable.sh"

Besides nullifiying .word _ZN6ClassA6squareEf, we've just added some explanatory comments to the source and to an output .ident ( = comment) tag.

Assemble that file:

$ arm-none-eabi-g++ -c ClassA_hacked.s

Then relink our program, inputting ClassA_hacked.o in lieu of ClassA.o:

$ arm-none-eabi-g++ main.o Wrapper.o ClassA_hacked.o -Wl,-gc-sections,-Map=mapfile --specs=rdimon.specs

It still works:

$ qemu-arm a.out
1.66667

And:

$ readelf --symbols --wide --demangle a.out | grep ClassA
  2274: 00000000     0 FILE    LOCAL  DEFAULT  ABS ClassA.cpp
  5113: 0006bb38    24 OBJECT  GLOBAL DEFAULT    4 vtable for ClassA
  5118: 000090b0    28 FUNC    WEAK   DEFAULT    2 ClassA::~ClassA()
  5398: 000090ac     4 FUNC    WEAK   DEFAULT    2 ClassA::~ClassA()
  5492: 000090ac     4 FUNC    WEAK   DEFAULT    2 ClassA::~ClassA()
  5855: 00009098    20 FUNC    GLOBAL DEFAULT    2 ClassA::reciprocal(float)
  6256: 00009088    16 FUNC    GLOBAL DEFAULT    2 Wrapper::setImplementation(ClassA*)

ClassA::square(float) is gone.

The linker mapfile shows that:

Discarded input sections
...[cut]...
.text._ZN6ClassA6squareEf
                0x00000000       0x14 ClassA_hacked.o
 .ARM.extab.text._ZN6ClassA6squareEf
                0x00000000        0x0 ClassA_hacked.o
 .ARM.exidx.text._ZN6ClassA6squareEf
                0x00000000        0x8 ClassA_hacked.o
...[cut]...
 

the function-section .text._ZN6ClassA6squareEf and its cohort were discarded.

And here's the confession note in the executable:

$ readelf -pment a.out

String dump of section 'ment':
  [     0]  GCC: (15:13.2.rel1-2) 13.2.1 20231009
  [    26]  Symbols [ _ZN6ClassA6squareEf ] were nullified in vtable _ZTV6ClassA by prune_vtable.sh

As you see, prune_vtable.sh is instruction-set neutral, but bound to GAS: it relies on GAS assembly format and directives. If it doesn't do what is asked it errors out.

Here is the same demo re-targeted to X86_64.

$ g++ --version | head -n1
g++ (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0

$ g++ -c *.cpp -Os -Wall -Wextra -pedantic -fno-rtti -ffunction-sections -fdata-sections -save-temps
$ ./prune_vtable.sh -v _ZTV6ClassA -s _ZN6ClassA6squareEf ClassA.s > ClassA_hacked.s
$ g++ -c ClassA_hacked.s
$ g++ main.o Wrapper.o ClassA_hacked.o -Wl,-gc-sections
$ readelf --symbols --wide a.out | grep square; echo Done
Done
$ ./a.out
1.66667

I checked that the script does the same job with assembly output by these other, differently targeted GCC C++ cross compilers:-

$ aarch64-linux-gnu-g++ --version | head -n1
aarch64-linux-gnu-g++ (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0

$ riscv64-unknown-elf-c++ --version | head -n1
riscv64-unknown-elf-c++ (13.2.0-11ubuntu1+12) 13.2.0

$ riscv32-unknown-elf-g++ --version | head -n1
riscv32-unknown-elf-g++ () 13.2.0

$ x86_64-w64-mingw32-g++ --version | head -n1
x86_64-w64-mingw32-g++ (GCC) 13-win32

$ i686-w64-mingw32-g++ --version | head -n1
i686-w64-mingw32-g++ (GCC) 13-win32

What happens if you accidentally nullify a vtable entry that is used by the program?

$ arm-none-eabi-g++ -c *.cpp -Os -Wall -Wextra -pedantic -fno-rtti -ffunction-sections -fdata-sections -save-temps
$ ./prune_vtable.sh -v _ZTV6ClassA -s _ZN6ClassA6squareEf -s _ZN6ClassA10reciprocalEf ClassA.s > ClassA_hacked.s

This time we're clobbering ClassA::reciprocal(float) as well as ClassA::square(float), which leaves the vtable defined as:

    .global _ZTV6ClassA
    .section    .rodata._ZTV6ClassA,"a"
    .align  2
    .type   _ZTV6ClassA, %object
    .size   _ZTV6ClassA, 24
_ZTV6ClassA:
    .word   0
    .word   0
    .word   _ZN6ClassAD1Ev
    .word   _ZN6ClassAD0Ev
/* Nullified next by prune_vtable.sh:   .word   _ZN6ClassA6squareEf */
    .word   0
/* Nullified next by prune_vtable.sh:   .word   _ZN6ClassA10reciprocalEf */
    .word   0
    

And:

$ arm-none-eabi-g++ main.o Wrapper.o ClassA_hacked.o -Wl,-gc-sections --specs=rdimon.specs
$ qemu-arm a.out
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
Segmentation fault (core dumped)

No surprise there.

If you got here seeking an answer to the question for g++ variant x86_64-linux-gnu, aarch64-linux-gnu, riscv64-unknown-elf or riscv32-unknown-elf, then you may prefer this one to my first answer.

That other answer aspires to work for the OP's arm-none-eabi-g++ compiler or any GCC C++ compiler as long as the GAS assembly of a polymorphic class's vtable looks the same modulo a few variations in the directives used. But that entails stepping in between the compiler and the linker to parse and modify assembly code, assembling and linking the edited code instead of the compiler's.

For g++ on x86_64-linux-gnu this step is avoidable, at least as of:

$ g++ --version | head -n1
g++ (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0   
$ realpath /usr/bin/g++
/usr/bin/x86_64-linux-gnu-g++-13

until further notice. The same applies to the matching GCC compilers for aarch64, riscv64 or riscv32. It is still unavoidable to know the assembly of the class's vtable preparatory to the fix (which of course is still completely unsanctioned by the C++ Standard).

With those provisos, in the example discussed in my other answer, the redundant virtual method ClassA::square(float)can be eliminated from ClassA's vtable, and hence eliminated from the linkage, by compiling and adding to the otherwise unchanged linkage the following source file:

$ cat ClassA_vtable.cpp
#include <cstddef>

extern std::ptrdiff_t const vtable_ent_2 asm("_ZN6ClassAD1Ev");
extern std::ptrdiff_t const vtable_ent_3 asm("_ZN6ClassAD0Ev");
extern std::ptrdiff_t const vtable_ent_5 asm("_ZN6ClassA10reciprocalEf");

extern std::ptrdiff_t const * const ClassA_vtable[6] asm("_ZTV6ClassA") 
__attribute__((aligned(8))) = {
    0,              // offset-to-top value, unchanged
    0,              // typeinfo pointer, unchanged
    &vtable_ent_2,  // ZN6ClassAD1Ev, unchanged
    &vtable_ent_3,  // _ZN6ClassAD0Ev, unchanged
    0,              // _ZN6ClassA6squareEf nullified
    &vtable_ent_5   // _ZN6ClassA10reciprocalEf, unchanged
};

It compiles to a drop-in replacement for ClassA's vtable, with the ClassA::square(float) slot zeroed out.

Compile and link:

$ g++ -c main.cpp ClassA.cpp Wrapper.cpp ClassA_vtable.cpp -Wall -Wextra -pedantic -fno-rtti -ffunction-sections -fdata-sections -save-temps
$ g++ main.o ClassA.o Wrapper.o ClassA_vtable.o -Wl,-gc-sections,-Map=mapfile

We get the right behaviour:

$ ./a.out
1.66667

And ClassA::square(float) is not to be seen:

$ readelf --symbols --demangle --wide a.out | grep ClassA
    15: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS ClassA.cpp
    19: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS ClassA_vtable.cpp
    25: 000000000000122a    29 FUNC    WEAK   DEFAULT   16 ClassA::~ClassA()
    26: 0000000000001248    47 FUNC    WEAK   DEFAULT   16 ClassA::~ClassA()
    27: 00000000000013ac    42 FUNC    GLOBAL DEFAULT   16 ClassA::reciprocal(float)
    46: 000000000000122a    29 FUNC    WEAK   DEFAULT   16 ClassA::~ClassA()
    50: 000000000000140e    26 FUNC    GLOBAL DEFAULT   16 Wrapper::setImplementation(ClassA*)
    57: 0000000000003d38    48 OBJECT  GLOBAL DEFAULT   24 vtable for ClassA

The linker mapfile shows that the function-section defining it in ClassA.o:

Discarded input sections
...[cut]...
.text._ZN6ClassA6squareEf
                0x0000000000000000       0x26 ClassA.o
...[cut]...

was garbage-collected.


Why does this work for some compilers and not others (including arm-none-eabi-g++, x86_64-w64-mingw32-g++ i686-w64-mingw32-g++)?

The obliging compilers are ones that always emit a weak symbol for a class vtable. E.g. in the assembly file ClassA.s preserved by -save-temps from that last compilation, the vtable _ZTV6ClassA is defined:

    .weak   _ZTV6ClassA
    .section    .data.rel.ro.local._ZTV6ClassA,"awG",@progbits,_ZTV6ClassA,comdat
    .align 8
    .type   _ZTV6ClassA, @object
    .size   _ZTV6ClassA, 48
_ZTV6ClassA:
    .quad   0
    .quad   0
    .quad   _ZN6ClassAD1Ev
    .quad   _ZN6ClassAD0Ev
    .quad   _ZN6ClassA6squareEf
    .quad   _ZN6ClassA10reciprocalEf
    

while our fix definition was assembled per ClassA_vtable.s as:

    .globl  _ZTV6ClassA
    .section    .data.rel.ro._ZTV6ClassA,"aw"
    .align 8
    .type   _ZTV6ClassA, @object
    .size   _ZTV6ClassA, 48
_ZTV6ClassA:
    .quad   0
    .quad   0
    .quad   _ZN6ClassAD1Ev
    .quad   _ZN6ClassAD0Ev
    .quad   0
    .quad   _ZN6ClassA10reciprocalEf
    

where the symbol is strongly global. As between a strong definition and a weak alternate, the linker honours the strong one and ignores the weak one.

The disobliging compilers are ones that prefer to emit a global symbol for a class vtable. When we try this fix on one of them we get a multiple definition linkage error:

$ x86_64-w64-mingw32-g++ -c main.cpp ClassA.cpp Wrapper.cpp ClassA_vtable.cpp -Wall -Wextra -pedantic -fno-rtti -ffunction-sections -fdata-sections
$ x86_64-w64-mingw32-g++ main.o ClassA.o Wrapper.o ClassA_vtable.o -Wl,-gc-sections
/usr/bin/x86_64-w64-mingw32-ld: ClassA_vtable.o:ClassA_vtable.:(.rdata$_ZTV6ClassA+0x0): multiple definition of `vtable for ClassA'; ClassA.o:ClassA.cpp:(.rdata$_ZTV6ClassA[_ZTV6ClassA]+0x0): first defined here
collect2: error: ld returned 1 exit status  

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744689916a4588147.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信