以下は私の実験です。本体と最後に4つの結論があります。
短縮版
一般的に言えば、関数を正常にオーバーライドするには、次のことを考慮する必要があります。
ロングバージョン
これらのソース ファイルがあります。
.
├── decl.h
├── func3.c
├── main.c
├── Makefile1
├── Makefile2
├── override.c
├── test_target.c
└── weak_decl.h
main.c
#include <stdio.h>
void main (void)
{
func1();
}
test_target.c
#include <stdio.h>
void func3(void);
void func2 (void)
{
printf("in original func2()\n");
}
void func1 (void)
{
printf("in original func1()\n");
func2();
func3();
}
func3.c
#include <stdio.h>
void func3 (void)
{
printf("in original func3()\n");
}
decl.h
void func1 (void);
void func2 (void);
void func3 (void);
weak_decl.h
void func1 (void);
__attribute__((weak))
void func2 (void);
__attribute__((weak))
void func3 (void);
override.c
#include <stdio.h>
void func2 (void)
{
printf("in mock func2()\n");
}
void func3 (void)
{
printf("in mock func3()\n");
}
Makefile1:
ALL:
rm -f *.o *.a
gcc -c override.c -o override.o
gcc -c func3.c -o func3.o
gcc -c test_target.c -o test_target_weak.o -include weak_decl.h
ar cr all_weak.a test_target_weak.o func3.o
gcc main.c all_weak.a override.o -o main -include decl.h
Makefile2:
ALL:
rm -f *.o *.a
gcc -c override.c -o override.o
gcc -c func3.c -o func3.o
gcc -c test_target.c -o test_target_strong.o -include decl.h # HERE -include differs!!
ar cr all_strong.a test_target_strong.o func3.o
gcc main.c all_strong.a override.o -o main -include decl.h
Makefile1 の結果の出力:
in original func1()
in mock func2()
in mock func3()
Makefile2 の出力:
rm *.o *.a
gcc -c override.c -o override.o
gcc -c func3.c -o func3.o
gcc -c test_target.c -o test_target_strong.o -include decl.h # -include differs!!
ar cr all_strong.a test_target_strong.o func3.o
gcc main.c all_strong.a override.o -o main -include decl.h
override.o: In function `func2':
override.c:(.text+0x0): multiple definition of `func2' <===== HERE!!!
all_strong.a(test_target_strong.o):test_target.c:(.text+0x0): first defined here
override.o: In function `func3':
override.c:(.text+0x13): multiple definition of `func3' <===== HERE!!!
all_strong.a(func3.o):func3.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
Makefile4:2: recipe for target 'ALL' failed
make: *** [ALL] Error 1
記号表:
all_weak.a:
test_target_weak.o:
0000000000000013 T func1 <=== 13 is the offset of func1 in test_target_weak.o, see below disassembly
0000000000000000 W func2 <=== func2 is [W]eak symbol with default value assigned
w func3 <=== func3 is [w]eak symbol without default value
U _GLOBAL_OFFSET_TABLE_
U puts
func3.o:
0000000000000000 T func3 <==== func3 is a strong symbol
U _GLOBAL_OFFSET_TABLE_
U puts
all_strong.a:
test_target_strong.o:
0000000000000013 T func1
0000000000000000 T func2 <=== func2 is strong symbol
U func3 <=== func3 is undefined symbol, there's no address value on the left-most column because func3 is not defined in test_target_strong.c
U _GLOBAL_OFFSET_TABLE_
U puts
func3.o:
0000000000000000 T func3 <=== func3 is strong symbol
U _GLOBAL_OFFSET_TABLE_
U puts
どちらの場合も、override.o
記号は次のとおりです。
0000000000000000 T func2 <=== func2 is strong symbol
0000000000000013 T func3 <=== func3 is strong symbol
U _GLOBAL_OFFSET_TABLE_
U puts
分解:
test_target_weak.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <func2>: <===== HERE func2 offset is 0
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # b <func2+0xb>
b: e8 00 00 00 00 callq 10 <func2+0x10>
10: 90 nop
11: 5d pop %rbp
12: c3 retq
0000000000000013 <func1>: <====== HERE func1 offset is 13
13: 55 push %rbp
14: 48 89 e5 mov %rsp,%rbp
17: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 1e <func1+0xb>
1e: e8 00 00 00 00 callq 23 <func1+0x10>
23: e8 00 00 00 00 callq 28 <func1+0x15>
28: e8 00 00 00 00 callq 2d <func1+0x1a>
2d: 90 nop
2e: 5d pop %rbp
2f: c3 retq
したがって、結論は次のとおりです。
ファイルで定義された関数は、.o
ファイルで定義された同じ関数をオーバーライドできます.a
。上記のMakefile1では、func2()
とfunc3()
inoverride.o
が対応する in をオーバーライドしますall_weak.a
。両方のファイルで試しまし.o
たが、うまくいきません。
GCCの場合、ここでVisual Studio toolchain.o
について述べたように、関数を個別のファイルに分割する必要はありません。上記の例でわかるように、(と同じファイル内) と(別のファイル内) の両方をオーバーライドできます。func2()
func1()
func3()
関数をオーバーライドするには、そのコンシューマーの翻訳単位をコンパイルするときに、その関数を weak として指定する必要があります。これにより、その機能が弱いものとして記録されますconsumer.o
。上記の例では、 andtest_target.c
を消費する をコンパイルするときに、andを weak として宣言するを追加する必要があります。も定義されていますが、問題ありません。func2()
func3()
-include weak_decl.h
func2()
func3()
func2()
test_target.c
さらなる実験
上記のソースファイルのままです。ただしoverride.c
、少し変更します。
override.c
#include <stdio.h>
void func2 (void)
{
printf("in mock func2()\n");
}
// void func3 (void)
// {
// printf("in mock func3()\n");
// }
ここで、 のオーバーライド バージョンを削除しましたfunc3()
。の元の実装にフォールバックしたいので、これを行いました。func3()
func3.c
私はまだMakefile1
構築するために使用します。ビルドはOKです。しかし、以下のように実行時エラーが発生します。
xxx@xxx-host:~/source/override$ ./main
in original func1()
in mock func2()
Segmentation fault (core dumped)
だから私は final のシンボルをチェックしましたmain
:
0000000000000696 T func1
00000000000006b3 T func2
w func3
func3
したがって、に有効なアドレスがないことがわかります。そのため、セグメントフォルトが発生します。
なぜ?func3.o
をall_weak.a
アーカイブ ファイルに追加しませんでしたか?
ar cr all_weak.a func3.o test_target_weak.o
から実装func2
を削除した で同じことを試しました。しかし、今回はセグメントフォルトはありません。func2
ovrride.c
override.c
#include <stdio.h>
// void func2 (void)
// {
// printf("in mock func2()\n");
// }
void func3 (void)
{
printf("in mock func3()\n");
}
出力:
xxx@xxx-host:~/source/override$ ./main
in original func1()
in original func2() <====== the original func2() is invoked as a fall back
in mock func3()
私の推測では、は と同じファイル/翻訳単位でfunc2
定義されているためです。Soは常に と一緒に持ち込まれます。そのため、リンカは常に解決できます。func1
func2
func1
func2
test_target.c
override.c
ただし、 の場合、別のファイル/翻訳単位(func3.c)func3
で定義されます。弱いと宣言された場合でも、コンシューマーは弱いと記録します。残念ながら、GCC リンカは、同じファイルの他のファイルをチェックして の実装を探しません。それは確かにありますが。test_target.o
func3()
.o
.a
func3()
all_weak.a:
func3.o:
0000000000000000 T func3 <========= func3 is indeed here!
U _GLOBAL_OFFSET_TABLE_
U puts
test_target_weak.o:
0000000000000013 T func1
0000000000000000 W func2
w func3
U _GLOBAL_OFFSET_TABLE_
U puts
したがって、オーバーライド バージョンを提供する必要があります。override.c
そうしfunc3()
ないと、解決できません。
しかし、なぜ GCC がこのように動作するのかはまだわかりません。説明できる人いたらお願いします。
(2021 年 8 月 8 日 9:01 AM 更新:
このスレッドでこの動作が説明される可能性があります。うまくいけば.)
したがって、さらなる結論は次のとおりです。
- 一部のシンボルをウィークとして宣言する場合は、すべてのウィーク関数のオーバーライド バージョンを提供することをお勧めします。そうしないと、発信者/消費者の同じファイル/翻訳単位内に存在しない限り、元のバージョンを解決できません。