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
|
Show 5 more comments
2 Answers
Reset to default 3Any 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
-ffunction-sections
– Ben Voigt Commented Mar 13 at 17:33