C (8) Linux Dynamic Library Compilation Framework

C (8) Grundgerüst für die Kompilierung dynamischer Linux-Bibliotheken

Autor: Once Day Datum: 5. August 2023

Es war ein langer Weg, aber jemand hat dich angelächelt ...

Referenz zitierte Dokumente:

1. Übersicht

Oftmals müssen wir eine Reihe grundlegender Bibliotheken für die Programmentwicklung entwickeln. Im Allgemeinen gibt es zwei Möglichkeiten, dies zu tun: Die eine ist die Verteilung des Quellcodes und die andere die Verteilung der Bibliotheksdateien. Was ich im Folgenden vorstellen werde, ist die Methode zur Verteilung von Bibliotheksdateien. Die Zielplattform ist ein Linux-System. Obwohl die Details der verschiedenen Systeme unterschiedlich sind, ist die Gesamtidee ähnlich.

Das gesamte Kompilierungsframework für dynamische Bibliotheken ist in drei Teile unterteilt:

  1. Die grundlegenden Quelldateien werden zum Aufbau einer abstrakten Schnittstellen-Abschirmungsschicht verwendet, die eine Grundlage für zukünftige systemübergreifende Transplantationen schafft.
  2. Kompilierungsskript, wie man die Ausgabe kompiliert und generiert, wie man erforderliche Kompilierungsoptionen hinzufügt.
  3. Das Testskript ist in das gesamte Kompilierungsframework integriert und generiert die erforderlichen Informationen.

Der aktuelle Gesamtrahmen ist noch relativ rudimentär, wichtig ist jedoch der gesamte Lernprozess und das Verständnis relevanter Kenntnisse und Konzepte.

Generell lässt sich der gesamte Projektrahmen in folgende Teile gliedern:

── MyProject
├── build	// 编译中间目录
│   ├── lib // 编译中间库文件输出
│   ├── src // 编译中间.d/.o文件
│   ├── test // 测试模块编译中间.d/.o文件, 
│   │   ├── test_app // 单独的测试工具模块
│   │   ├── src //测试模块会单独再编译一份内嵌的源码
│   │   ├── test // 测试模块
│   │   │   └── auto_unit // 单元测试
│   │   └── util // 测试模块代码
│   └── util // 工具类代码
├── crash // 本地执行(单元/集成)测试coredump文件目录
├── include // 只对外的头文件
├── output // 编译输出文件
│   └── usr
│       ├── bin // 二进制工具文件
│       ├── include //对外提供的头文件
│       ├── lib // 动态/静态库文件
│       └── test // 一些测试文件
├── port // 源码运行环境层接口
├── src  // 实际源码
├── test // 测试源码
│   ├── auto_unit //单元测试
│   └── test_app  //独立测试源码
├── tool // 工具源码
└── util // 通用工具代码
......(编译+脚本+说明+许可证+日志+...等信息)

Das Obige ist eine gängige Verzeichnisstruktur für die Softwareentwicklung. Jede Software ist anders, aber die Grundidee ist ähnlich. Der Punkt ist, dass jeder Abschnitt in einem separaten Ordner abgelegt werden sollte, damit er leichter verwaltet werden kann.

2. Kompilierungsgrundlagen

2.1 Kompilierungsabhängigkeiten

Im Vergleich zum bloßen Schreiben einer Software erfordert die Erstellung einer Standard-Open-Source-Softwarebibliothek eine strenge Analyse der Kompilierungsabhängigkeiten. Der Schwerpunkt liegt hier auf der glibc-Bibliothek, da andere Bibliotheken sehr explizit abhängig sind.

Im Allgemeinen wird der Compiler mit einigen Bibliotheksdateien geliefert , bei denen es sich um die originellsten Header-Dateien handelt, aber stdio.hsie gehören offensichtlich nicht dazu. Die grundlegendste Abhängigkeitsbeziehung besteht darin, dass Sie stdio.hsich auf die libc-Bibliothek verlassen müssen, sobald Sie eine ähnliche Standard-C-Header-Datei verwenden. Dies ist immer noch nicht sehr genau. Beispielsweise stellen die Datenbank libm.so und die Thread-Bibliothek libpthread.so zusätzliche Abhängigkeiten dar. Auf dieser Grundlage müssen einige GNU-Erweiterungsfunktionen und Syntax unterschieden werden.

Der erste ist die Abhängigkeit vom Posix-Standard. Die Standardkompilierung weist die geringste Skalierbarkeit auf, unterstützt nur den C-Standard und die höchste Portabilität mit allgemeiner Definition XOPEN_SOURCEund _GNU_SOURCEKontrolle.

_XOPEN_SOURCEist ein Makro, das beim Kompilieren eines C- oder C++-Programms definiert werden kann. Dieses Makro wird verwendet, um die in den X/Open- und POSIX-Standards definierten Funktionen zu aktivieren, einschließlich vieler Unix- und Linux-Systemaufrufe und Bibliotheksfunktionen.

Abhängig _XOPEN_SOURCEvom definierten Wert von können Funktionen in verschiedenen Versionen der X/Open- und POSIX-Standards aktiviert werden. Zum Beispiel:

  • Bei der Definition als 500 ist die mit SUSv2 (Single UNIX Specification Version 2) und POSIX.1 1996 kompatible Funktionalität aktiviert.
  • Bei der Definition als 600 sind SUSv3 (Single UNIX Specification Version 3) und POSIX.1 2001-kompatible Funktionen aktiviert.
  • Bei der Definition als 700 sind SUSv4 (Single UNIX Specification Version 4) und POSIX.1 2008-kompatible Funktionen aktiviert.

Normalerweise wird dieses Makro in der Befehlszeile des Compilers oder über eine Präprozessoranweisung am Anfang des Quellcodes wie folgt definiert:

#define _XOPEN_SOURCE 700

Durch die Definition dieses Makros kann sichergestellt werden, dass Ihr Code auf verschiedene Unix-ähnliche Systeme portierbar ist, da dadurch sichergestellt wird, dass Ihr Code nur die in den X/Open- und POSIX-Standards definierten Funktionen verwendet.

Tatsächlich kann es direkt so definiert werden, CFLAGS += -D_GNU_SOURCEdass es alle GNU-Funktionserweiterungen im größtmöglichen Umfang unterstützt und die Unterstützung für atomare Operationen/Thread-Bibliotheken/Datei-E/A/String-Funktionen verbessert. Für normal entwickelte Software gelten keine so hohen Transplantationsanforderungen. Die Implementierung dieser Grundfunktionen kommt einer Neuerfindung des Rades gleich. Sofern nicht erforderlich, ist es in der Regel besser, sie direkt wiederzuverwenden.

2.2 C-Sprachversion

Im Allgemeinen ist Ihnen die C-Sprachversion, die im Grunde C99 ist, egal, aber es gibt ein Missverständnis. Denn im Allgemeinen wird die erweiterte Syntax von gcc abgedeckt. Tatsächlich kann die reine C99-Standardsyntax bestimmte Anforderungen wie anonyme Strukturen/Unions nicht erfüllen. Daher wird derzeit empfohlen, C11 direkt als Basisstandard zu verwenden.

Aktivieren Sie dann genügend Kompilierungsoptionen. Die folgenden sind gängige Kompilierungsoptionen:

CFLAGS += -Wall -Wextra -Werror# 严格的错误处理策略
CFLAGS += -fstack-protector-all# 保护函数栈
CFLAGS += -std=c11# 使用C11标准, c99不包含匿名结构体和联合体
CFLAGS += -g3# 生成调试信息, 默认-g(g2), 更详细可以-g3
CFLAGS += -O0# 不进行优化

Für gängige Kompilierungsszenarien reichen die oben genannten Optionen aus, aber das reicht nicht aus. Wir müssen weitere zusätzliche Optionen aktivieren, wie folgt.

CFLAGS += -Wshadow# 检查变量声明遮蔽
CFLAGS += -Wundef# 检查未定义的宏
CFLAGS += -Wcast-qual# 启用去除类型限定符造成的警告
CFLAGS += -Wcast-align# 启用类型强制转换可能破坏对齐的警告
CFLAGS += -Wstrict-prototypes# 严格检查函数原型
CFLAGS += -Wmissing-prototypes# 启用未声明的函数原型的警告
CFLAGS += -Wmissing-declarations# 启用未声明的全局函数和变量的警告。
# CFLAGS += -Wredundant-decls# 启用多余的声明的警告。
CFLAGS += -Wnested-externs# 启用嵌套的外部声明的警告
CFLAGS += -Wunreachable-code# 启用不可达代码的警告。
CFLAGS += -Wuninitialized# 检查未初始化的变量
CFLAGS += -Winline# 检查内联函数, 无法内联将报错
CFLAGS += -Wfloat-equal# 检查浮点数比较
CFLAGS += -Wswitch# 检查switch语句中的枚举值, switch case集合必须和枚举定义集合一致
CFLAGS += -Wswitch-default# 检查switch语句中的default, 没有default将报错
CFLAGS += -Wbad-function-cast# 检查函数指针强制转换
CFLAGS += -Waggregate-return# 不允许返回结构体或联合体
CFLAGS += -Wpacked# 检查结构体或联合体的字节对齐, 如果对齐不合理, 将报错
CFLAGS += -Wpadded# 检查结构体或联合体的字节对齐, 如果额外填充了字节, 将报错
CFLAGS += -Wvariadic-macros# 检查宏定义
CFLAGS += -Wvla# 检查变长数组
CFLAGS += -Wconversion# 检查隐式类型转换
CFLAGS += -Wsign-conversion# 检查隐式类型转换
CFLAGS += -Wpointer-arith# 检查指针运算
CFLAGS += -Wwrite-strings# 检查字符串常量赋值给非const指针
CFLAGS += -Woverlength-strings# 检查字符串常量长度
CFLAGS += -Wpedantic# 严格遵循ISO C/C++标准,检查编译器扩展语法
#CFLAGS += -pedantic-errors# 严格遵循ISO C/C++标准,检查编译器扩展语法
#CFLAGS += -pedantic-errors -Wno-error=pedantic# 只警告不错误
CFLAGS += -Wformat-overflow=2# 检查格式字符串是否存在溢出的风险
CFLAGS += -Wformat=2# 检查printf和scanf函数的格式字符串
CFLAGS += -Walloc-zero# 检查malloc和calloc函数的参数, 不能为0

Tatsächlich -Wall -Wextra -Werrorermöglichen die drei Optionen eine große Anzahl von Kompilierungsprüfungen. Die meisten der oben genannten Compileroptionen sind andere Prüfoptionen, die ausgeschlossen sind . Wenn Sie so viele Prüfungen aktivieren, wird es schwierig, jeden Schritt des Codes zu schreiben, da C-Code wirklich willkürlich sein kann, z. B. Zeiger und Typkonvertierungen, Variablendeklarationen usw. Die strengen Anforderungen dieser Art von Code stellen eine Art Selbstdisziplin dar. Nur durch die Pflege strenger Gewohnheiten kann die Codequalität verbessert werden.

Darüber hinaus müssen Sie beim Drucken von Funktionen mit variablen Parametern die Übereinstimmung zwischen Formatzeichenfolge und Parametertyp wie folgt überprüfen:

void my_printf(void *my_object, int my_value, const char *my_format, ...) 
    __attribute__((format(printf, 3, 4)));

__attribute__((format(printf, 3, 4)))Der dritte Parameter der angegebenen Funktion ist eine Formatzeichenfolge und der vierte Parameter ist der Anfang der Parameterliste dieser Formatzeichenfolge. Der Compiler prüft my_printf, ob die Verwendung des dritten Parameters beim Aufruf printfden Formatzeichenfolgenregeln von entspricht , zum Beispiel:

my_printf(my_obj, 42, "%d %s", my_int, my_string);  // Correct
my_printf(my_obj, 42, "%d %s", my_int);  // Warning: format '%s' expects a matching 'char *' argument

Diese Eigenschaft ist sehr nützlich, da sie dabei helfen kann, einige häufige Probleme zu erkennen, die durch die falsche Verwendung von Formatzeichenfolgen verursacht werden. Obwohl es nicht Teil des Standard-C/C++ ist, wird es in GCC und einigen anderen kompatiblen Compilern weitgehend unterstützt.

2.3 GCC-Compiler-Attributspezifikation

Zusätzlich zur normalen C-Syntax sind auch einige Methoden für die Kommunikation und den Betrieb mit dem Compiler erforderlich, wie folgt:

#pragma GCC diagnostic ignored "-Wpedantic" /* 忽略所有pedantic错误 */

/**
 * 定义空类型, 其在复合数据结构体里面不占"空间", H[0]是gnu扩展语法, 这里不能使用.
 * Intentional empty struct definition.
 * 虽然C11标准允许空结构体,但在实践中,空结构体常常是由于漏写结构体成员列表而导致的错误。
 * 所以,为了帮助发现这类错误,-Werror=pedantic 选项会把空结构体当成一个错误来报告。
 * 这里使用预处理指令屏蔽报错.
 */

typedef struct empty {
    /* 如果非要有一个元素, 那么会造成一些麻烦, 编译期会检查该结构体是否为零字节 */
} empty_t;

#pragma GCC diagnostic error   "-Wpedantic" /* 恢复pedantic错误 */

Hierbei werden durch #pragmaAnweisungen die vom Compiler überprüften Parameter kurzzeitig geändert, um Fehler zu vermeiden. Durch diese Art der subtilen Anpassung können die Kompilierungsergebnisse besser kontrolliert werden.

Hier sind einige andere Eigenschaftssteuerelemente:

  • Attribut ((Konstruktor)) und Attribut ((Destruktor)): Mit diesen beiden Attributen kann festgelegt werden, dass die Funktion automatisch aufgerufen wird, bevor das Programm mit der Ausführung beginnt (Konstruktor) oder nachdem es die Ausführung beendet (Destruktor).
  • Attribut ((gepackt)): Dieses Attribut wird verwendet, um das Speicherlayout der Struktur zu steuern, um sie so klein wie möglich zu machen. Normalerweise richtet der Compiler die Felder einer Struktur aus, um die Zugriffsgeschwindigkeit zu optimieren, was dazu führen kann, dass zusätzlicher Speicherplatz beansprucht wird. Das gepackte Attribut weist den Compiler an, diese Struktur nicht auszurichten.
  • Attribut ((aligned(n))): Dieses Attribut wird verwendet, um die Mindestausrichtungsanforderungen für Variablen oder Strukturen anzugeben. Das Attribut ((aligned(16))) stellt beispielsweise sicher, dass die Adresse des Objekts im Speicher ein Vielfaches von 16 ist.
  • Attribut ((noreturn)): Dieses Attribut wird verwendet, um anzugeben, dass die Funktion niemals zurückkehrt. Dies ist für Funktionen wie Exit oder Abort nützlich, da der Compiler für solche Funktionen spezielle Optimierungen durchführen kann.
  • Attribut ((veraltet)): Dieses Attribut wird verwendet, um Funktionen oder Variablen zu markieren, die veraltet sind. Der Compiler generiert eine Warnung, wenn Sie versuchen, eine Funktion oder Variable zu verwenden, die mit dem Attribut „deprecated“ gekennzeichnet ist.
  • Attribut ((unbenutzt)): wird verwendet, um anzugeben, dass eine Funktion, ein Funktionsparameter oder eine Variable nicht verwendet werden darf. Ohne dieses Attribut generiert der Compiler möglicherweise eine Warnung, da nicht verwendete Parameter oder Variablen normalerweise auf einen Programmierfehler hinweisen.
  • attribute ((hot)) und attribute ((cold)) : Diese beiden Attribute geben dem Compiler einen Hinweis darauf, wie oft eine Funktion ausgeführt wird. hotEigenschaften geben an, dass die Funktion häufig aufgerufen wird, undcoldEigenschaften geben an, dass die Funktion selten aufgerufen wird. Diese Informationen helfen dem Compiler bei der Entscheidung, wie der Code optimiert werden soll. Beispielsweise können häufig aufgerufene Funktionen vom Compiler zusammengelegt werden, um den CPU-Cache zu nutzen, während selten aufgerufene Funktionen möglicherweise an einer entfernten Stelle im Programm platziert werden.
  • Attribut ((alias)) : Dieses Attribut kann Aliase für Funktionen definieren. Wenn Sie beispielsweise über eine Funktion verfügenoriginal_function, können Sie einen Alias ​​dafür definierenalias_function

Es gibt viele ähnliche Attribute. Einzelheiten finden Sie in der gcc-Dokumentation (oben befindet sich ein Link zum Referenzdokument).

2.4 Atombezogene Operationen

Variablen vom atomaren Typ werden im Allgemeinen volatilemithilfe von Modifikatoren geändert, müssen jedoch im Allgemeinen selbst implementiert werden und atomic.hverwenden einfach Dateien.

Die von gcc unterstützten atomaren Funktionen sind in zwei Typen unterteilt: einer ist die alte GCC-Erweiterungsfunktion und der andere ist die neue Funktion des C11-Standards.

Funktionen wie die folgenden __sync_xxxx sind alte GNU-Erweiterungen, die von GCC 4.1.2 unterstützt werden :

__sync_fetch_and_add, __sync_fetch_and_sub, __sync_fetch_and_or,__sync_fetch_and_and, __sync_fetch_and_xor, __sync_fetch_and_nand

Diese Art von Funktion gibt zunächst den alten Wert der Variablen zurück und führt dann eine bestimmte Operation an der Variablen aus. Beispielsweise __sync_fetch_and_addgibt eine Funktion den alten Wert einer Variablen zurück und fügt dann die Variable hinzu.

__sync_add_and_fetch, __sync_sub_and_fetch, __sync_or_and_fetch, __sync_and_and_fetch, __sync_xor_and_fetch, __sync_nand_and_fetch

Dieser Funktionstyp führt zunächst eine bestimmte Operation an einer Variablen aus und gibt dann nach der Operation den Wert zurück. Beispielsweise __sync_add_and_fetchfügt eine Funktion eine Variable hinzu und gibt den resultierenden Wert zurück.

__sync_bool_compare_and_swap, __sync_val_compare_and_swap

Mit diesen beiden Funktionen werden Vergleichs- und Austauschoperationen durchgeführt. __sync_bool_compare_and_swapGibt einen booleschen Wert zurück, der angibt, ob der Vorgang erfolgreich war. __sync_val_compare_and_swapGibt den alten Wert der Variablen zurück.

__sync_lock_test_and_set, __sync_lock_release

Mit diesen beiden Funktionen werden einfache Sperren implementiert. __sync_lock_test_and_setSetzt eine Sperre oder gibt einen Wert ungleich Null zurück, wenn die Sperre bereits gesetzt ist.

GCC 4.9.0 begann, __atomic_xxxx-Funktionen wie __atomic_fetch_add usw. zu unterstützen. Als Teil von C11 sind die neuen Funktionen kompatibler als die alten Funktionen .

  • atomic_init: Diese Funktion wird verwendet, um ein atomares Objekt zu initialisieren. Zum Beispiel:

    atomic_int ai;
    atomic_init(&ai, 0);
    
  • atomic_store: Diese Funktion speichert einen Wert atomar. Das heißt, in einer Multithread-Umgebung wird der Vorgang während der Ausführung nicht durch andere Threads unterbrochen. Zum Beispiel:

    atomic_int ai;
    atomic_store(&ai, 10);
    
  • atomic_load: Diese Funktion lädt einen Wert atomar. Genau atomic_storewie , ist dieser Vorgang atomar. Zum Beispiel:

    atomic_int ai = ATOMIC_VAR_INIT(10);
    int i = atomic_load(&ai);
    
  • atomic_fetch_addSumme atomic_fetch_sub: Diese beiden Funktionen führen Additions- und Subtraktionsoperationen atomar aus und geben den Wert vor der Operation zurück. Zum Beispiel:

    atomic_int ai = ATOMIC_VAR_INIT(10);
    int i = atomic_fetch_add(&ai, 5); // i 现在是 10,ai 现在是 15
    int j = atomic_fetch_sub(&ai, 3); // j 现在是 15,ai 现在是 12
    
  • atomic_compare_exchange_strong: Diese Funktion versucht, Werte atomar zu vergleichen und auszutauschen. Wenn der aktuelle Wert des atomaren Objekts gleich dem Wert von „expected“ ist, wird der Wert des Objekts auf den Wert „wunsch“ gesetzt und „true“ zurückgegeben; andernfalls wird der Wert „expected“ auf den aktuellen Wert des Objekts gesetzt und „false“ zurückgegeben ist zurück gekommen. Zum Beispiel:

    atomic_int ai = ATOMIC_VAR_INIT(10);
    int expected = 10;
    bool success = atomic_compare_exchange_strong(&ai, &expected, 15);
    // 如果 ai 是 10,那么现在 ai 是 15,并且 success 是 true
    // 否则,expected 现在是 ai 的值,并且 success 是 false
    
  • atomic_fetch_or: Führen Sie atomar eine bitweise ODER-Operation aus (d. h. „bitweises ODER“) und geben Sie den Wert vor der Operation zurück. Zum Beispiel:

    atomic_int ai = ATOMIC_VAR_INIT(0b0011);
    int i = atomic_fetch_or(&ai, 0b0101); // i 现在是 0b0011,ai 现在是 0b0111
    
  • atomic_fetch_xor: Führen Sie atomar eine bitweise XOR-Operation (dh „Bit-XOR“) durch und geben Sie den Wert vor der Operation zurück. Zum Beispiel:

    atomic_int ai = ATOMIC_VAR_INIT(0b0011);
    int i = atomic_fetch_xor(&ai, 0b0101); // i 现在是 0b0011,ai 现在是 0b0110
    
  • atomic_fetch_and: Führt atomar eine bitweise UND-Operation (d. h. „bitweises UND“) durch und gibt den Wert vor der Operation zurück. Zum Beispiel:

    atomic_int ai = ATOMIC_VAR_INIT(0b0011);
    int i = atomic_fetch_and(&ai, 0b0101); // i 现在是 0b0011,ai 现在是 0b0001
    
  • atomic_flag_test_and_set: Dies ist eine atomic_flagspezielle Funktion für den Typ. Es wird atomar atomic_flagauf „true“ gesetzt und gibt seinen vorherigen Wert zurück. Zum Beispiel:

    atomic_flag af = ATOMIC_FLAG_INIT;
    bool prev = atomic_flag_test_and_set(&af); // prev 是 false,af 现在是 true
    
  • atomic_flag_clear: Dies ist auch eine atomic_flagspezielle Funktion für den Typ. Es wird atomar atomic_flagauf „false“ gesetzt. Zum Beispiel:

    atomic_flag af = ATOMIC_FLAG_INIT;
    atomic_flag_test_and_set(&af);
    atomic_flag_clear(&af); // af 现在是 false
    

In C11 können Sie eine „Speicherreihenfolge“<stdatomic.h> für atomare Operationen in festlegen. Dies bestimmt, wie Zugriffe auf den Speicher geordnet werden (d. h. in welcher Reihenfolge sie für andere Threads sichtbar sind) .

  • memory_order_relaxed( __ATOMIC_RELAXED): Dies ist die schwächste Speicherreihenfolge. Es gibt keine Synchronisierungs- oder Bestellgarantien. Nur die atomare Operation selbst führt garantiert nicht zu Datenwettläufen.
  • memory_order_consume( __ATOMIC_CONSUME): Diese Speicherreihenfolge garantiert, dass alle nachfolgenden Lesevorgänge, die auf dem Ergebnis dieser Operation basieren, nur den Speicherstatus vor dieser Operation sehen. Es kann jedoch nicht garantiert werden, dass andere Threads die Ergebnisse dieses Vorgangs sehen.
  • memory_order_acquire( __ATOMIC_ACQUIRE): Diese Speicherreihenfolge garantiert, dass alle Lese- und Schreibvorgänge nach diesem Vorgang nur den Speicherstatus vor diesem Vorgang sehen. Dies ist ein gängiges Synchronisierungsprimitiv, das zum Schutz des Eintrags eines kritischen Abschnitts verwendet wird.
  • memory_order_release( ): Diese Speicherreihenfolge garantiert, dass dieser Vorgang und alle Lese- und Schreibvorgänge davor abgeschlossen werden, bevor __ATOMIC_RELEASEalle nachfolgenden memory_order_acquireoder -Vorgänge ausgeführt werden. memory_order_consumeDies ist ein gängiges Synchronisierungsprimitiv, das zum Schutz des Ausgangs eines kritischen Abschnitts verwendet wird.
  • memory_order_acq_rel( __ATOMIC_ACQ_REL): Diese Speichersequenz ist eine Kombination aus memory_order_acquireund memory_order_release. Es garantiert, dass dieser Vorgang und alle Vorgänge vor ihm abgeschlossen werden, bevor nachfolgende Vorgänge auf der Grundlage der Ergebnisse dieses Vorgangs erfolgen.
  • memory_order_seq_cst( __ATOMIC_SEQ_CST): Dies ist die stärkste Speicherreihenfolge. Es sorgt für sequentielle Konsistenz, was bedeutet, dass alle Threads die gleiche Reihenfolge der Vorgänge sehen.

In C11 können durch die oben genannten unterschiedlichen Speicherreihenfolgen effizientere gleichzeitige Operationen erreicht werden, das Risiko ist jedoch sehr hoch, sodass die oben genannten atomaren Funktionen standardmäßig in der strengsten Reihenfolge SEQ_CSTausgeführt werden .

Darüber hinaus können Sie auch eine Vollspeicherbarriere festlegen:

/**
 * 全内存屏障(Full Memory Barrier):
 * __sync_synchronize() 是 GCC 提供的一种内建函数(built-in function),
 * 用于在多线程环境中实现全内存屏障(Full Memory Barrier)。
 * 它确保在函数调用之前的所有内存访问操作(读和写)在函数调用之后的所有内存访问操作之前完成。
 * 换句话说,它阻止了处理器重新排序跨越该屏障的内存操作。
 */
#define xxx_rmb() __sync_synchronize()
#define xxx_wmb() __sync_synchronize()
2.5 Behauptung und aktiver Abbruch

Um schwerwiegende Auswirkungen von Geschäftsproblemen zu vermeiden, muss das Programm in der frühen Entwicklung von Testcode und im täglichen Betrieb bei Erkennung eines schwerwiegenden Fehlers direkt beendet werden, anstatt weiter ausgeführt zu werden. Im Allgemeinen wird die Abbruchfunktion verwendet, um den Vorgang aktiv zu beenden. Die unterste Schicht sendet das entsprechende Signal SIGABRT an ihren eigenen Thread.

Der Beurteilungsprozess wird als Behauptung bezeichnet und in zwei verschiedenen Zeiträumen ausgeführt, dem Kompilierungszeitraum und dem Ausführungszeitraum. Behauptungen zur Kompilierungszeit bestimmen hauptsächlich, ob einige Datenstrukturen und Makros normal sind, während die Laufzeit hauptsächlich bestimmt, ob einige Dateninformationen den Erwartungen entsprechen.

Assertionsfunktionen zur Kompilierungszeit können im Allgemeinen selbst geschrieben werden, wie unten definiert (es gibt viele Definitionen von Schreibmethoden, die Sie selbst auswählen können):

/**
 * 编译期检查上面的静态数组是否大小合适。FPN_ASSERT()是运行期检查,不适合这里.
 * 条件成立编译期会报错. 由于严格的C标准不允许0 size, 因此需要改成-1.
 */
#define XXX_ASSERT(cond) ((void)(sizeof(int[(-(!!(cond) ? 1 : -1))])))

Dies dient dazu, zu bestätigen, dass das Ziel festgelegt werden sollte (d. h. ein Fehler wird gemeldet, wenn es nicht festgelegt wird). Eine gewisse Logik besteht darin, zu bestätigen, dass die Bedingung festgelegt ist und ein Fehler gemeldet wird. Dieser Teil kann entsprechend festgelegt werden deine eigenen Gewohnheiten.

Laufzeitassertionen können die Headerdatei <assert.h> in der Standardbibliothek ausleihen, die bereits eine Reihe von Assertionsfunktionen gekapselt hat.

/* 符合C标准的编译器断言检测函数 */
#include <assert.h>
#define xxx_assert(cond)       assert(cond)
#define xxx_assert_perror(str) assert_perror(str)
#define xxx_abort() abort()
2.6 Optionen zur Bibliothekskompilierung

Unter Linux folgen dynamische Bibliotheken normalerweise einer Versionskontrollstrategie, die als „Drei-Ebenen-Versionsmechanismus“ bezeichnet wird. Diese Strategie wird durch Dateinamen umgesetzt. Konkret sieht der vollständige Name einer Bibliothek normalerweise so aus: libname.so.major.minor.patch, wobei:

  • libnameist der Basisname der Bibliothek.
  • majorist die Hauptversionsnummer. Die Hauptversionsnummer wird erhöht, wenn inkompatible Änderungen an der öffentlichen Schnittstelle der Bibliothek vorgenommen werden.
  • minorDies ist die Nebenversionsnummer. Wenn neue Funktionen hinzugefügt werden und gleichzeitig die Abwärtskompatibilität gewahrt bleibt, wird die Nebenversionsnummer erhöht.
  • patchist die Revisionsnummer. Wenn abwärtskompatible Fehlerbehebungen vorgenommen werden, wird die Revisionsnummer erhöht.

In dieser Versionskontrollstrategie gibt es zwei wichtige symbolische Links:

  1. libname.so: Dies ist für Compiler und Linker und verweist normalerweise auf die neueste Version der in der Entwicklung verwendeten Bibliothek. Programme werden beim Kompilieren mit dieser Datei verknüpft.
  2. libname.so.major: Dies ist für die Verwendung durch Laufzeitlinker (z. B. ld.sooder ld-linux.so) gedacht, die normalerweise eine Verknüpfung mit der neuesten Version der Bibliothek herstellen, die binärkompatibel ist. Das Programm verlinkt bei der Ausführung auf diese Datei.

Dieser Mechanismus ermöglicht es Entwicklern und Benutzern, verschiedene Versionen einer Bibliothek gleichzeitig zu installieren und zu verwenden, ohne sich gegenseitig zu beeinträchtigen. Beispielsweise kann ein Programm von einem libname.so.1.0.0anderen Programm verwendet werden libname.so.2.0.0, während es gleichzeitig auf demselben System ausgeführt wird, sofern diese unterschiedliche Hauptversionsnummern haben.

Das im Allgemeinen entsprechende Makefile lautet wie folgt:

# myapp版本号定义: major(关键版本) + minor(次要版本)
# 1.0.0: 2023年6月30日, 最初始的版本
MAJOR_VERSION = 2
MINOR_VERSION = 2
PATCH_VERSION = 2
export TARGET_VERSION = $(MAJOR_VERSION).$(MINOR_VERSION).$(PATCH_VERSION)

# 编译目标的名字
export TARGET_NAME = myapp++

# 动态库对象的短识别名称, 用于识别库的主要版本
TARGET_SO_NAME = lib$(TARGET_NAME).so
# 动态库对象的soname名称
TARGET_SO_SONAME = $(TARGET_SO_NAME).$(MAJOR_VERSION)
# 动态库对象的完整识别名称, 用于识别库的次要版本
TARGET_SO_FULL_NAME = $(TARGET_SO_NAME).$(TARGET_VERSION)

# 动态库对应的CFLAGS选项
SO_CFLAGS = -fPIC
SO_LD_FLAGS = -shared -Wl,-soname,$(TARGET_SO_SONAME) $(LDFLAGS)
# 强调链接器生成动态库时添加一个build id, 唯一识别
SO_LD_FLAGS += -Wl,--build-id

# 动态库文件版本信息文件
SD_VERSION_FILE =$(TOP_DIR)/version.s
SO_LD_FLAGS += -Wl,--version-script=$(SD_VERSION_FILE)

# 编译目标: 动态库文件的命名
TARGET_SO = $(BUILD_LIB)/$(TARGET_SO_FULL_NAME)

# 编译目标: 静态库文件的命名
TARGET_LIB_NAME = lib$(TARGET_NAME).a
TARGET_LIB_FULL_NAME = $(TARGET_LIB_NAME).$(TARGET_VERSION)
TARGET_LIB = $(BUILD_LIB)/$(TARGET_LIB_FULL_NAME)

# 静态库对应的CFALGS选项:
# -r 将文件替换到库中。如果文件已经存在于库中,它将被更新。如果文件不存在于库中,它将被添加。
# -c 在需要时创建新库。如果库文件不存在,它将被创建。
# -s 创建符号表。这将为库生成一个符号表,加快链接过程。
AR_CFLAGS = -rcs

-Wl,-soname,$(TARGET_SO_SONAME)Es wird verwendet, um den Namen des externen Links der angegebenen Bibliothek zu finden. Nach der Kompilierung lautet der generierte Dateiname wie folgt:

libmyapp.so.2.2.2

Dann müssen Sie zusätzliche Softlink-Symboldateien generieren:

# 安装动态库文件, 用于在目标设备上运行
install-target: $(TARGET_SO) | build-dirs
	@echo "***** start to install files as following *****"
	@echo "    SO file: "$(TARGET_SO)
	@echo "    LIB DIR: "$(OUTPUT_LIB)
	@echo "    INCLUDE: "$(OUTPUT_INCLUDE)
	$(INSTALL) -m 755 $(TARGET_SO)
	cd $(OUTPUT_LIB) \
		&& ln -sf $(TARGET_SO_FULL_NAME) $(TARGET_SO_SONAME) \
		&& ln -sf $(TARGET_SO_SONAME) $(TARGET_SO_NAME)

Die endgültige Form lautet wie folgt. Dies ist das Standardformat für die Verteilung dynamischer Linux-Bibliotheksdateien:

libmyapp.so.2 -> libmyapp.so.2.2
libmyapp.so.2.2 -> libmyapp.so.2.2.2
libmyapp.so.2.2.2
2.7 Gesamt-Makefile

Das gesamte Makefile enthält viele Details, auf die ich hier nicht näher eingehen werde. Sie sind unten aufgeführt:

###
 # @Author: Once day
 # @Date: 2023-06-13 18:06
 # @LastEditTime: 2023-06-28 11:06
 # Encoder=utf-8,Tabsize=4,Eol=\r\n.
 # Email:[email protected]
###

# 定义make源文件搜索目录
#- 当前目录优先级最高
#- 目录由冒号分割
#- 按照从左到右的顺序依次查找
# VPATH = src:../headers

# 使用export导出的变量, 可以在子makefile中使用

# 定义当前文件夹, 是内建变量, 无需定义
export TOP_DIR = $(CURDIR)

# .PHONE伪目标
.PHONY: all
# 要生成的目标文件
all: build

# 文件头部的

# 静态添加C源文件
export SRCS
SRCS += util/llqueue.c
SRCS += src/myapp-async.c
SRCS += src/myapp-business.c
SRCS += src/myapp-context.c
SRCS += src/myapp-log.c

# 动态添加C外部文件
HEADERS = $(wildcard include/*.h)
HEADERS += src/myapp-public.h

# 定义编译输出文件夹
ifneq ($(OUTPUT_DIR),y)
export OUTPUT_DIR = $(TOP_DIR)/output
else
export OUTPUT_DIR = $(OUTPUT)
endif

# 定义编译输出的可执行程序子目录
export OUTPUT_BIN = $(OUTPUT_DIR)/usr/bin

# 定义编译输出的头文件子目录
export OUTPUT_INCLUDE = $(OUTPUT_DIR)/usr/include

# 定义编译输出的库文件子目录
export OUTPUT_LIB = $(OUTPUT_DIR)/usr/lib

# 定义编译输出的测试文件子目录
export OUTPUT_TEST = $(OUTPUT_DIR)/usr/test

# 定义编译临时文件夹
export BUILD_DIR = $(TOP_DIR)/build
# 定义编译输出的库文件子目录
export BUILD_LIB = $(BUILD_DIR)/lib

# 定义二进制目标输出文件名
OBJS := $(SRCS:%.c=$(BUILD_DIR)/%.o)

# 定义不存在的二级/三级/... 子目录, 一级目录会自动创建
CREATE_ALL_DIRS := $(sort $(dir $(OBJS)))
CREATE_ALL_DIRS += $(BUILD_LIB) $(OUTPUT_BIN) $(OUTPUT_INCLUDE) $(OUTPUT_LIB) $(OUTPUT_TEST)

# 创建输出文件夹的子目录
$(CREATE_ALL_DIRS):
	mkdir -p $@

# 定义C编译器和对应选项(LD直接无法链接, 还需指定部分二进制文件, 所以使用gcc代替)
# 设置编译根目录
ifneq ($(CC),y)
export CC = gcc
endif
ifneq ($(LD),y)
export LD = gcc
endif
ifneq ($(AR),y)
export AR = ar
endif
ifneq ($(INSTALL),y)
export INSTALL = install
endif

# 定义C编译选项参数 https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
CFLAGS += -Wall -Wextra -Werror# 严格的错误处理策略
CFLAGS += -Wshadow# 检查变量声明遮蔽
CFLAGS += -Wundef# 检查未定义的宏
CFLAGS += -Wcast-qual# 启用去除类型限定符造成的警告
CFLAGS += -Wcast-align# 启用类型强制转换可能破坏对齐的警告
CFLAGS += -Wstrict-prototypes# 严格检查函数原型
CFLAGS += -Wmissing-prototypes# 启用未声明的函数原型的警告
CFLAGS += -Wmissing-declarations# 启用未声明的全局函数和变量的警告。
# CFLAGS += -Wredundant-decls# 启用多余的声明的警告。
CFLAGS += -Wnested-externs# 启用嵌套的外部声明的警告
CFLAGS += -Wunreachable-code# 启用不可达代码的警告。
CFLAGS += -Wuninitialized# 检查未初始化的变量
CFLAGS += -Winline# 检查内联函数, 无法内联将报错
CFLAGS += -Wfloat-equal# 检查浮点数比较
CFLAGS += -Wswitch# 检查switch语句中的枚举值, switch case集合必须和枚举定义集合一致
CFLAGS += -Wswitch-default# 检查switch语句中的default, 没有default将报错
CFLAGS += -Wbad-function-cast# 检查函数指针强制转换
CFLAGS += -Waggregate-return# 不允许返回结构体或联合体
CFLAGS += -Wpacked# 检查结构体或联合体的字节对齐, 如果对齐不合理, 将报错
CFLAGS += -Wpadded# 检查结构体或联合体的字节对齐, 如果额外填充了字节, 将报错
CFLAGS += -Wvariadic-macros# 检查宏定义
CFLAGS += -Wvla# 检查变长数组
CFLAGS += -Wconversion# 检查隐式类型转换
CFLAGS += -Wsign-conversion# 检查隐式类型转换
CFLAGS += -Wpointer-arith# 检查指针运算
CFLAGS += -Wwrite-strings# 检查字符串常量赋值给非const指针
CFLAGS += -Woverlength-strings# 检查字符串常量长度
CFLAGS += -Wpedantic# 严格遵循ISO C/C++标准,检查编译器扩展语法
#CFLAGS += -pedantic-errors# 严格遵循ISO C/C++标准,检查编译器扩展语法
#CFLAGS += -pedantic-errors -Wno-error=pedantic# 只警告不错误
CFLAGS += -Wformat-overflow=2# 检查格式字符串是否存在溢出的风险
CFLAGS += -Wformat=2# 检查printf和scanf函数的格式字符串
CFLAGS += -Walloc-zero# 检查malloc和calloc函数的参数, 不能为0
CFLAGS += -fstack-protector-all# 保护函数栈
CFLAGS += -std=c11# 使用C11标准, c99不包含匿名结构体和联合体
CFLAGS += -g3# 生成调试信息, 默认-g(g2), 更详细可以-g3
CFLAGS += -O0# 不进行优化

# 定义C编译器预定义宏, 开启XOPEN_SOURCE宏, 使得编译器能够识别POSIX扩展语法
#	不定义:_XOPEN_SOURCE
#		仅支持 C 标准。最大限度地提高可移植性。
#	500:_XOPEN_SOURCE>=500
#		支持 XPG5(相当于 SUSv2)标准。提供基本的 POSIX 兼容性。
#	600:_XOPEN_SOURCE>=600
#		支持 XSI(X/Open System Interface),相当于 SUSv3/POSIX.1-2001。提供较高POSIX兼容性。
#	700:_XOPEN_SOURCE>=700
#		支持最新 X/Open 标准(现为 SUSv4/POSIX.1-2008)。提供最大POSIX兼容性,但可移植性降低。
# _GNU_SOURCE, 在linux环境下直接开启全部的GNU扩展函数支持, 增强对原子操作/线程库/文件IO/字符串函数的支持
export CFLAGS += -D_GNU_SOURCE

# 强调此刻正在编译, 将影响IDE解析的部分还原定义
CFLAGS += -D_myapp_COMPILING

# 定义C编译器头文件搜索目录, 不需要定位头文件的话可以注释掉
export INCLUDE = -I$(TOP_DIR)
INCLUDE += -I$(TOP_DIR)/util
INCLUDE += -I$(TOP_DIR)/src
INCLUDE += -I$(TOP_DIR)/port
INCLUDE += -I$(TOP_DIR)/include

# 定义C编译器库文件搜索目录, 不需要定位库文件的话可以注释掉
export LIB = -L$(OUTPUT_LIB)

# 定义所需要的静态或动态库文件
export LIBS = -lc -lpthread -levent

# myapp版本号定义: major(关键版本) + minor(次要版本)
# 1.0.0: 2023年6月30日, 最初始的版本
MAJOR_VERSION = 2
MINOR_VERSION = 2
PATCH_VERSION = 2
export TARGET_VERSION = $(MAJOR_VERSION).$(MINOR_VERSION).$(PATCH_VERSION)

# 编译目标的名字
export TARGET_NAME = myapp++

# 动态库对象的短识别名称, 用于识别库的主要版本
TARGET_SO_NAME = lib$(TARGET_NAME).so
# 动态库对象的soname名称
TARGET_SO_SONAME = $(TARGET_SO_NAME).$(MAJOR_VERSION)
# 动态库对象的完整识别名称, 用于识别库的次要版本
TARGET_SO_FULL_NAME = $(TARGET_SO_NAME).$(TARGET_VERSION)

# 动态库对应的CFLAGS选项
SO_CFLAGS = -fPIC -D_myapp_SHARED
SO_LD_FLAGS = -shared -Wl,-soname,$(TARGET_SO_SONAME) $(LDFLAGS)
# 强调链接器生成动态库时添加一个build id, 唯一识别
SO_LD_FLAGS += -Wl,--build-id

# 动态库文件版本信息文件
SD_VERSION_FILE =$(TOP_DIR)/version.s
SO_LD_FLAGS += -Wl,--version-script=$(SD_VERSION_FILE)

# 编译目标: 动态库文件的命名
TARGET_SO = $(BUILD_LIB)/$(TARGET_SO_FULL_NAME)

# 编译目标: 静态库文件的命名
TARGET_LIB_NAME = lib$(TARGET_NAME).a
TARGET_LIB_FULL_NAME = $(TARGET_LIB_NAME).$(TARGET_VERSION)
TARGET_LIB = $(BUILD_LIB)/$(TARGET_LIB_FULL_NAME)

# 静态库对应的CFALGS选项:
# -r 将文件替换到库中。如果文件已经存在于库中,它将被更新。如果文件不存在于库中,它将被添加。
# -c 在需要时创建新库。如果库文件不存在,它将被创建。
# -s 创建符号表。这将为库生成一个符号表,加快链接过程。
AR_CFLAGS = -rcs

# 设置编译根目录
ifeq ($(SYSROOT),y)
SYSROOT_DIR = --sysroot=$(SYSROOT)
else
SYSROOT_DIR =
endif

# 最终需要编译的目标, 暂时不需要静态库#$(TARGET_LIB)
TARGET = $(TARGET_SO) $(TARGET_LIB)

# 编译调试信息丰富度
# export VERBOSE = --verbose
# export LD_VERBOSE = -Wl,--verbose
# export AR_VERBOSE = -v

# 动态库文件依赖关系
$(TARGET_SO): $(OBJS) | $(CREATE_ALL_DIRS)
	$(LD) -o $@ $^ $(SO_LD_FLAGS) $(LIB) $(LIBS) $(LD_VERBOSE) $(SYSROOT_DIR)

# 可执行文件依赖关系
$(TARGET_LIB): $(OBJS) | $(CREATE_ALL_DIRS)
	$(AR) -o $@ $^ $(AR_CFLAGS) $(AR_VERBOSE)

# 首先通过GCC -MMD和-MP选项自动生成源文件的依赖关系文件(.d文件)
DEPS=$(SRCS:%.c=$(BUILD_DIR)/%.d)

# 二进制头文件依赖关系, 使用静态模式, 在固定的集合里匹配
# -MMD: 生成依赖文件,记录源文件依赖的头文件列表。
#	这个选项会自动生成一个 .d 文件,包含源文件依赖的所有头文件, 同时编译出.o文件。
# -MP 告诉 GCC 在编译过程中更新依赖文件。这意味着如果头文件有变更,GCC 会重新编译使用了这个头文件的源代码文件。
# -M 会生成 Makefile 所需的依赖文件,包含所有头文件以及系统文件(如 /usr/include/)。这个选项生成的依赖文件包含较详细的信息,但也比较冗长。
# -MM 会生成仅包含用户头文件(不包含系统头文件)的依赖文件。这个选项生成的依赖文件更加简洁
# -MF:这个选项用于指定生成的依赖关系文件的名称。它后面紧跟一个文件名,该文件名通常具有 .d 扩展名。
#    例如:-MF file.d。当你使用 -M 或 -MM 生成依赖关系时,-MF 选项告诉编译器将结果输出到指定的文件中
# -MT:这个选项用于指定目标名称。它后面紧跟一个字符串,这个字符串表示在生成的依赖关系文件中使用的目标名称。
#    例如:-MT "obj/file.o dep/file.d"。这将在依赖关系文件中使用 obj/file.o dep/file.d 作为目标名称。
#    这是有用的,因为你可以为多个目标(例如目标文件和依赖文件)指定相同的依赖关系。
#
$(DEPS): $(BUILD_DIR)/%.d: %.c | $(CREATE_ALL_DIRS)
	$(CC) -MM $(CFLAGS) $(INCLUDE) $< -MF $@ -MT "$(@:.d=.o) $@" $(SYSROOT_DIR)

# 包含这些.d依赖文件
include $(DEPS)

# 二进制文件依赖关系, 每个.c单独编译二进制文件, 使用静态模式, 在固定的集合里匹配
$(OBJS): $(BUILD_DIR)/%.o: %.c | $(CREATE_ALL_DIRS)
	$(CC) -c $< -o $@ $(CFLAGS) $(SO_CFLAGS) $(INCLUDE) $(VERBOSE) $(SYSROOT_DIR)

# 重新(增量)编译
build: $(TARGET)

# 重新生成头文件依赖关系
build-dirs: $(CREATE_ALL_DIRS)

# 重新编译, 由于不能循环依赖,因此必须重开第二个make执行build
rebuild: clean
	$(MAKE) build

# 安装库文件
install: $(TARGET_SO) | build-dirs
	@echo "***** start to install files as following *****"
	@echo "    SO file: "$(TARGET_SO)
	@echo "    AR file: "$(TARGET_LIB)
	@echo "    LIB DIR: "$(OUTPUT_LIB)
	@echo "    INCLUDE: "$(OUTPUT_INCLUDE)
	$(INSTALL) -m 755 $(TARGET_SO)
	cd $(OUTPUT_LIB) \
		&& ln -sf $(TARGET_SO_FULL_NAME) $(TARGET_SO_SONAME) \
		&& ln -sf $(TARGET_SO_SONAME) $(TARGET_SO_NAME)
	$(INSTALL) -m 755 $(TARGET_LIB) $(OUTPUT_LIB)
	cd $(OUTPUT_LIB) && ln -sf $(TARGET_LIB_FULL_NAME) $(TARGET_LIB_NAME)
	$(INSTALL) -m 644 $(HEADERS) $(OUTPUT_INCLUDE)

# 安装静态库+动态库文件, 用于开发相关程序
install-devel: $(TARGET_SO) | build-dirs
	@echo "***** start to install files as following *****"
	@echo "    SO file: "$(TARGET_SO)
	@echo "    AR file: "$(TARGET_LIB)
	@echo "    LIB DIR: "$(OUTPUT_LIB)
	@echo "    INCLUDE: "$(OUTPUT_INCLUDE)
	$(INSTALL) -m 755 $(TARGET_SO)
	cd $(OUTPUT_LIB) \
		&& ln -sf $(TARGET_SO_FULL_NAME) $(TARGET_SO_SONAME) \
		&& ln -sf $(TARGET_SO_SONAME) $(TARGET_SO_NAME)
	$(INSTALL) -m 755 $(TARGET_LIB) $(OUTPUT_LIB)
	cd $(OUTPUT_LIB) && ln -sf $(TARGET_LIB_FULL_NAME) $(TARGET_LIB_NAME)
	$(INSTALL) -m 644 $(HEADERS) $(OUTPUT_INCLUDE)

# 安装动态库文件, 用于在目标设备上运行
install-target: $(TARGET_SO) | build-dirs
	@echo "***** start to install files as following *****"
	@echo "    SO file: "$(TARGET_SO)
	@echo "    LIB DIR: "$(OUTPUT_LIB)
	@echo "    INCLUDE: "$(OUTPUT_INCLUDE)
	$(INSTALL) -m 755 $(TARGET_SO)
	cd $(OUTPUT_LIB) \
		&& ln -sf $(TARGET_SO_FULL_NAME) $(TARGET_SO_SONAME) \
		&& ln -sf $(TARGET_SO_SONAME) $(TARGET_SO_NAME)

# 动态库文件(输出)的依赖关系, 由于不能循环依赖,因此必须重开第二个make执行build
$(OUTPUT_LIB)/$(TARGET_SO_NAME): $(TARGET_SO)
	@echo "Lose some files: $(TARGET_SO)"
	$(MAKE) install

# 静态库文件(输出)的依赖关系, 由于不能循环依赖,因此必须重开第二个make执行build
$(OUTPUT_LIB)/$(TARGET_LIB_NAME): $(TARGET_LIB)
	@echo "Lose some files: $(TARGET_LIB)"
	$(MAKE) install

# 安装库文件的依赖关系, 不能依赖于伪目标, 伪目标总是执行
install-lib: $(OUTPUT_LIB)/$(TARGET_SO_NAME) $(OUTPUT_LIB)/$(TARGET_LIB_NAME)

# 测试目录
TEST_DIR = test

# 指定编译测试代码程序, 依赖于当前myapp库文件编译完毕
$(TEST_DIR)-%: | $(TARGET)
	$(MAKE) -C $(TEST_DIR)/$(subst -, ,$*)

# 获取所有的测试程序的Makefile文件
TEST_MAKEFILES = $(wildcard $(TEST_DIR)/*/Makefile)

# 获取所有测试程序的目标集合
TEST_TARGETS = $(patsubst %/Makefile, %, $(TEST_MAKEFILES))

# 编译所有的测试程序, 依赖于当前myapp库文件编译完毕
test-build: $(TARGET)
	$(foreach var, $(TEST_TARGETS), $(MAKE) -C $(var) all)

# 运行指定的测试程序, 依赖于当前myapp库文件编译完毕
run-test-%:
	$(MAKE) -C $(TEST_DIR)/$* all
	@echo "***** start to run test *****"
	ulimit -c unlimited && bash $(TOP_DIR)/run.sh $(OUTPUT_TEST)/$*

# 开始进行单元测试
TEST_OBJS = $(wildcard $(OUTPUT_TEST)/*)
test: $(TEST_OBJS) | install-lib
	@echo "***** start to run test *****"
	ulimit -c unlimited && bash $(TOP_DIR)/run.sh $<

retest: $(TEST_OBJS) | install-lib test-build
	$(MAKE) test

# 清除生成的目标文件
clean:
	rm -rf $(BUILD_DIR)
.PHONY: clean

clean-all:
	rm -rf $(BUILD_DIR) $(OUTPUT_DIR)

# 打包命令, 将源码里重要的文件打包
TAR_SRCS := src/ include/ util/ port/ Makefile
TAR_SRCS += .gitignore
TAR_SRCS += run.sh
TAR_SRCS += version.log
TAR_SRCS += version.s

# 打包myapp库文件
tar:
	@mkdir -p $(TARGET_NAME) && cp -rf $(TAR_SRCS) $(TARGET_NAME)
	tar -czvf $(OUTPUT_DIR)/lib$(TARGET_NAME).$(TARGET_VERSION).tar.gz $(TARGET_NAME)
	rm -rf $(TARGET_NAME)

2.8 Unterstützung des laufenden Skripts

Standardmäßig generiert Linux keine Kerndateien und auch der dynamische Bibliothekspfad muss entsprechend geändert werden. Daher müssen Sie vor dem Ausführen der Testdatei ein Skript verwenden, um eine geeignete Umgebung zu generieren, wie folgt:

###
 # @Author: Once day
 # @Date: 2023-07-01 15:19:40
 # @LastEditTime: 2023-07-11 11:07
 # Encoder=utf-8,Tabsize=4,Eol=\r\n.
 # Email:[email protected]
###

# 构建myapp运行环境
export LD_LIBRARY_PATH=./output/usr/lib:$LD_LIBRARY_PATH

# 设置coredump文件的生成路径
# ulimit -c

# 允许生成coredump文件
# echo "|gzip -c > ${PWD}/crash/%e-%p-%t-%s-${TARGET_VERSION}.coredump.gz" | \
#    sudo tee /proc/sys/kernel/core_pattern
STR="${
     
     PWD}/crash/%e-%p-%t-%s-${TARGET_VERSION}.coredump"
sudo bash -c "echo '${STR}' > /proc/sys/kernel/core_pattern"

# 创建crash文件夹
if [ ! -d ${
    
    PWD}/crash ]; then
    mkdir -p ${
    
    PWD}/crash
fi

# 开始执行所有文件
echo -e "***** start run all test programs *****\n"

# 内存检测工具 Valgrind:
# --tool=memcheck: 使用valgrind的memcheck内存检测工具
# --leak-check=full: 进行完整的内存泄漏检测
# --show-leak-kinds=all: 显示所有类型的内存泄漏
# --track-origins=yes: 跟踪内存泄漏的来源,即泄漏发生的具体位置
# 所以总结起来,这个valgrind命令的作用是:
#   使用memcheck工具,进行完整和详细的内存泄漏检测,显示所有的内存泄漏信息,并跟踪泄漏发生的准确位置。
#   通过使用这些参数,可以非常方便和详细地调试程序中的内存泄漏问题。
#   memcheck工具会打印每个泄漏发生的堆栈信息,以及泄漏的大小等信息。
MEMCHECK="valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all --track-origins=yes"

# 计数变量
count=0
# 遍历传入的所有参数
for program_path in $@
do
    # 计数
    count=$[$count + 1]
    program_name=$(basename $program_path)
    echo -e "[${count}]run file: ${program_name}"
    $MEMCHECK $program_path
    echo -e "\n"
done

# 执行完毕
echo "***** run all test file end *****"

3. Referenz zu Glibc-Kenntnissen

3.1 Ausblenden interner Symbole
First of all, you need to have the function prototyped somewhere,
say in foo/foo.h:

int foo (int __bar);

If calls to foo within libc.so should always go to foo defined in libc.so,
then in include/foo.h you add:

libc_hidden_proto (foo)

line and after the foo function definition:

int foo (int __bar)
{
return __bar;
}
libc_hidden_def (foo)

Das Obige ist ein Text im Glibc-Quellcode, der eine Methode zum Ausblenden von Symbolen enthüllt. Unser Programm erfordert keine so komplizierte Implementierung, sodass wir einen Teil davon extrahieren können, um ein einfaches Framework zum Ausblenden von Symbolen zu erstellen. wie folgt:

/**
 * 设置符号可见性属性:
 * 符号有四种属性, 分别是默认属性(default), 隐藏属性(hidden), 受保护属性(protected), 内部属性(internal).
 *  -default:这是符号的默认可见性。在这种情况下,符号在链接时可被其他模块(如共享库)访问和引用。
 *      这相当于没有指定可见性属性。
 *  -hidden:这表示符号在链接时被隐藏,不会被其他模块访问和引用。这有助于实现封装和隐藏实现细节。
 *      请注意,即使符号被声明为 extern,也可以使用此属性将其隐藏。
 *  -protected:这表示符号在链接时具有受保护的可见性。这意味着符号可以被其他模块访问,但不能被覆盖。
 *      这个属性在创建共享库时有用,可以确保库中的符号不会被应用程序或其他库的符号覆盖。
 *  -internal:这表示符号具有内部链接的可见性。这意味着符号只在当前模块内可见,并且在链接时会被丢弃。
 *      这个属性可以用于优化目的,例如,当编译器可以确定符号只在当前源文件中使用时,可以使用此属性减小生成的代码。
 */
#define __hidden_attr(sym) __attribute__((visibility(#sym)))

Sie können unterschiedliche Sichtbarkeiten für Programmsymbole festlegen, die sich in der Binärdatei widerspiegeln. Mit readelf können Sie erkennen, dass verschiedene Symbole unterschiedliche sichtbare Bereiche haben. Die folgenden Typen:

  • default: Dies ist die Standardsichtbarkeit des Symbols. In diesem Fall kann zum Linkzeitpunkt von anderen Modulen (z. B. gemeinsam genutzten Bibliotheken) auf das Symbol zugegriffen und darauf verwiesen werden. Dies entspricht der Nichtangabe eines Sichtbarkeitsattributs.
  • hidden: Dies bedeutet, dass das Symbol zum Zeitpunkt der Verknüpfung ausgeblendet ist und von anderen Modulen nicht aufgerufen und referenziert wird. Dies trägt dazu bei, eine Kapselung zu erreichen und Implementierungsdetails auszublenden. Beachten Sie, dass ein Symbol, selbst wenn es als extern deklariert ist, mit diesem Attribut ausgeblendet werden kann.
  • protected: Dies zeigt an, dass das Symbol zum Linkzeitpunkt geschützte Sichtbarkeit aufweist. Das bedeutet, dass das Symbol von anderen Modulen zwar angesprochen, aber nicht überschrieben werden kann. Diese Eigenschaft ist beim Erstellen gemeinsam genutzter Bibliotheken nützlich, um sicherzustellen, dass Symbole in der Bibliothek nicht durch Symbole aus der Anwendung oder anderen Bibliotheken überschrieben werden.
  • internal: Dies zeigt an, dass das Symbol über eine interne Linksichtbarkeit verfügt. Dies bedeutet, dass Symbole nur innerhalb des aktuellen Moduls sichtbar sind und zum Linkzeitpunkt verworfen werden. Dieses Attribut kann zu Optimierungszwecken verwendet werden, um beispielsweise den generierten Code zu reduzieren, wenn der Compiler feststellen kann, dass das Symbol nur in der aktuellen Quelldatei verwendet wird.

Die zweite besteht darin, Linknamen auf Assembly-Ebene zu generieren:

/**
 * 隐藏的汇编链接符号名, __USER_LABEL_PREFIX__ 为链接符号前缀, 一般为 _.
 * 也可能为‘’空字符, 例如在arm平台上, __USER_LABEL_PREFIX__ 为空字符.
 * 一般来说, C语言层次看到的name和汇编层级是可以不一样, 例如C语言层次看到的name为foo,
 * 但是汇编层次看到的name为_foo, 这样就可以通过__USER_LABEL_PREFIX__来区分.
 */
#define __hidden_asm_name(name)          __hidden_asm_name1(__USER_LABEL_PREFIX__, name)
#define __hidden_asm_name1(prefix, name) __hidden_asm_name2(prefix, name)
#define __hidden_asm_name2(prefix, name) #prefix name

Für verschiedene Plattformen können Funktionssymbole auf Assembly-Ebene ein definiertes, normalerweise leeres Präfixsymbol tragen __USER_LABEL_PREFIX__, das dem im C-Quellcode definierten Funktionsnamen entspricht.

Zusammenfassend lässt sich sagen, dass es die Sichtbarkeit der ursprünglichen C-Quellcodesymbole verbirgt und die Namen seiner Symbole auf Assemblyebene ändert, sodass die Symbole von außen abgeschirmt und intern aufgerufen werden können.

/**
 * 隐藏内部函数, hidden属性只允许内部函数在内部源代码中间共享使用.
 * name是C语言中函数(变量)符号的定义, internal是汇编中函数(变量)符号的定义.
 * __asm__将name定义的符号重命名为internal定义的符号, 从而隐藏name定义的符号.
 * 因为在多个C源文件之间, 看到的是汇编层级的符号, 所以需要通过__asm__来重命名.
 */
#define __hidden_proto(name, internal) \
    extern __typeof__(name) name __asm__(__hidden_asm_name(#internal)) __hidden_attr(hidden)

/* 隐藏内部函数, hidden属性只允许内部函数在内部源代码中间共享使用 */
#define xxx_hidden_proto(name) __hidden_proto(name, __NI_##name)

Dies xxx_hidden_protoist die endgültige Definition der Header-Dateideklaration, die die Sichtbarkeit des C-Symbols einschränkt und einen neuen Namen für das Assemblysymbol gibt.

Definieren Sie dann ein intern verwendetes Symbol in der C-Quelldatei wie folgt neu:

/**
 * 重定义原有符号和汇编符号的关系, 使得外部函数可以调用内部函数.
 * local是被隐藏的内部函数名称, internal(=name)是其他源文件中间可以调用的函数名称.
 * 对于其他源文件来说, 汇编时看到的符号依旧是name(总是直接调用name())), 但是链接时会找不到符号.
 * 下面以一个实例说明其实现机制:
 *  (1) 首先(在头文件里)声明一个内部函数, 通过__hidden_proto将其隐藏起来, 使得其他源文件无法调用.
 *      1. int foo(int a, int b); 定义了一个全局符号(函数), 但是没有强调是外部符号.
 *          源文件链接时这类符号必须要在内部汇编文件找到其定义.
 *          如果找不到定义, 链接器会报出未定义符号的错误.
 *      2. __hidden_proto(foo, __NI_foo);
 *          定义了一个全局符号(函数), 但是强调是内部符号, 实际上还将foo重命名为__NI_foo(汇编符号)。
 *          源文件在链接时会以__NI_foo作为需要链接的全局符号. 在C符号层级,
 *          依然可以使用foo作为全局符号. 简单来说, 链接时会使用__NI_foo作为目标符号去搜索.
 * (2) 然后(在源文件里)定义一个foo实例函数, 通过_xxx_hidden_def将其重定义为外部函数.
 *      1. int foo(int a, int b) { return a + b; }
 *          定义了一个全局符号(函数), 但是没有强调是外部符号.
 *          源文件链接时这类符号必须要在内部汇编文件找到其定义.
 * (3) 定义外部调用符号, 让外部模块能以foo符号调用内部的foo函数.
 *      1. xxx_hidden_def(foo);
 *          首先定义了一个全局符号, 其在C层级为__EI_foo, 其在汇编层级为foo.
 *          然后又指定全局符号__EI_foo(foo)是(汇编符号)__NI_foo的别名.
 *          因此, 全局汇编符号foo和本地隐藏汇编符号__NI_foo是等价的.
 * 如果没有第(3)步操作, 那么foo函数就只能在内部各模块之间调用了, 即链接之前的汇编阶段。
 * 一旦链接之后, 其他模块就无法调用foo函数了, 因为链接器找不到foo函数的定义(符号表为内部函数,
 * 且名称为__NI_foo). 进行了第三部操作之后, 会产生一个全局符号__EI_foo, 其在C层级为__EI_foo,
 * 其在汇编层级为foo. 所以内部模块链接完毕之后, 会在符号表增加一个全局汇编符号(foo),
 * 该符号可供外部模块进行链接调用. 原来的本地汇编符号(__NI_foo)则供内部模块进行调用,
 * foo地址和__NI_foo地址是一样的. 原因很简单, 汇编层级上, foo符号只是__NI_foo符号的一个别称,
 * 在不同的模块中会使用不同的名称. 下面是一个简单的逻辑图, 说明了上述过程的最终结果:
 *           C层级符号 ->(汇编, AS)->  汇编层级符号   ->(链接)->   可重定位(可执行)文件
 * 外部        foo                   foo(global)                 foo(global)
 * 内部(1)     foo                  __NI_foo(hidden)           __NI_foo(hidden)
 * 重定义(3)  __EI_foo(global)      foo(global)                foo(global)
 * 别名(3)    __NI_foo(hidden)      foo(global)                __NI_foo(hidden)
 *
 * 整体调用过程如下:
 *  内部模块: foo(C源码) -> __NI_foo(汇编符号) -> foo(可执行代码段, C源码foo函数体)
 *  外部模块: foo(C源码) -> foo(汇编符号) -> __NI_foo(链接符号, 别名) -> foo(可执行代码段,
 * C源码foo函数体)
 */
#define __hidden_ver1(local, internal, name)                                 \
    extern __typeof(name) __EI_##name __asm__(__hidden_asm_name(#internal)); \
    extern __typeof(name) __EI_##name __attribute__((alias(__hidden_asm_name(#local))))

/* 需要对外导出的函数(变量)符号需要使用该定义 */
#define xxx_hidden_def(name) __hidden_ver1(__NI_##name, name, name)

/* 如果对外导出的符号名称有变化, 那么使用该定义 */
#define xxx_hidden_def_rename(name, rename) __hidden_ver1(__NI_##name, rename, name)

Hierbei ist zu beachten, dass xxx_hidden_proto(func)die Header-Datei, in der sich die Definition befindet, für externe Dateien unsichtbar ist. Tatsächlich werden die interne Definition und die externe Definition getrennt und sind dann auf der zugrunde liegenden Assembly-Ebene gleichwertig. Hier ist eine Zusammenfassung:

  • Das interne Modul enthält xxx_hidden_proto(func)die Definitions-Header-Datei, daher ist func im internen Modul eine versteckte interne Funktion und der tatsächliche Linkname lautet __NI__func.
  • Externe Module enthalten keine xxx_hidden_proto(func)Definitionsheaderdateien, daher versuchen externe Module, funcFunktionen zu verknüpfen.
  • Im internen Modul ist ein Symbol definiert __EI_func, das __NI_funcein Alias ​​von ist, und sein Assembly-Symbol ist func, sodass die Funktion extern verknüpft wird, __EI_funcum funcden Aufruf von zu realisieren.

funcWie aus diesem Schritt hervorgeht, können der spezifische Name in der externen Header-Datei und der Name der tatsächlichen internen Funktion völlig unterschiedlich sein, sodass der Unterschied zwischen den beiden leicht abgeschirmt werden kann.

3.2 Versionskontrolle von Funktionssymbolen

Unterteilt in zwei Situationen: Standardversion und angegebene Version (Syntax des GNU-Linkers zum Angeben von Symbolversionen):

  • name@version: Diese Syntax wird verwendet, um eine bestimmte Version eines Symbols zu definieren. Wenn der Linker auf diese Syntax stößt, ordnet er diese Version des Symbolnamens der bereitgestellten Version zu. Wenn anderer Code auf dieses Symbol verweist, kann er auf diese Weise diese spezifische Version explizit anfordern. Beachten Sie, dass bei mehreren Versionen einer Symboldefinition die Verwendung der Name@Version-Syntax dazu führt, dass der Linker eine bestimmte Version auswählt.
  • name@@version: Diese Syntax wird verwendet, um die Standardversion eines Symbols zu definieren. Wenn der Linker auf diese Syntax stößt, ordnet er die Standardversion des Symbolnamens der bereitgestellten Version zu. Das bedeutet, dass der Linker diese Standardversion auswählt, wenn anderer Code auf dieses Symbol verweist, wenn eine bestimmte Version nicht explizit angefordert wird. Ein Symbol kann nur eine Standardversion haben, verwenden Sie name@@version
/**
 * 导出函数带版本定义,用于区分不同版本的接口:
 *  1. real: 真实的函数名称
 *  2. name: 函数名称
 *  3. version: 版本号
 */
/* 指定版本定义 */
#define symbol_version_reference(real, name, version) \
    __asm__(".symver " #real "," #name "@" #version)

#define symbol_version(real, name, version) symbol_version_reference(real, name, version);

/* 默认版本定义 */
#define _default_symbol_version(real, name, version) \
    __asm__(".symver " #real "," #name "@@" #version)

#define default_symbol_version(real, name, version) _default_symbol_version(real, name, version);

Dies wird im Allgemeinen nicht häufig verwendet, kann jedoch in Notfällen verwendet werden. Die relevanten Details müssen in den Details der LD-Arbeit verstanden werden.

Nach dem Kompilieren in eine dynamische Bibliothek können Sie readelf --wide -s xxx.soden Symbolstatus der Zielbibliothek anzeigen.

3.3 LD-Link-Versionsskript

Zusätzlich zum manuellen Festlegen der Symbolsichtbarkeit und -version mithilfe von Compiler-Eigenschaften. Die Sichtbarkeit externer Symbole kann auch über die Skripte des LD-Linkers gesteuert werden.

# 动态库文件版本信息文件
SD_VERSION_FILE =$(TOP_DIR)/version.s
SO_LD_FLAGS += -Wl,--version-script=$(SD_VERSION_FILE)

version.s ist eine speziell kompilierte Versionskontrollskriptdatei wie folgt:

# 库版本控制脚本, 详细信息参考以下文档:
# https://sourceware.org/binutils/docs/ld/VERSION.html

VERS_1.1 {
	 global:
		 foo1;
	 local:
		 old*;
		 original*;
		 new*;
};

VERS_1.2 {
		 foo2;
} VERS_1.1;

VERS_2.0 {
		 bar1; bar2;
	 extern "C++" {
		 ns::*;
		 "f(int, double)";
	 };
} VERS_1.2;

Das Versionsskript definiert einen Baum von Versionsknoten. Geben Sie Knotennamen und gegenseitige Abhängigkeiten im Versionsskript an. Es ist möglich anzugeben, welche Symbole an welche Versionsknoten gebunden sind, und der angegebene Satz von Symbolen kann auf einen lokalen Bereich reduziert werden, sodass sie außerhalb der gemeinsam genutzten Bibliothek nicht global sichtbar sind.

Dieses Beispielversionsskript definiert drei Versionsknoten. Der erste definierte Versionsknoten ist VERS_1.1; er hat keine weiteren Abhängigkeiten. Das Skript foo1bindet das Symbol an VERS_1.1. Es reduziert einige Symbole auf den lokalen Bereich, sodass sie außerhalb der gemeinsam genutzten Bibliothek nicht sichtbar sind; dies erfolgt mithilfe eines Platzhaltermusters, sodass jedes Symbol, dessen Name mit oldoder beginnt, abgeglichen originalwird . newKann

Das verwendete Platzhaltermuster ist dasselbe wie das Platzhaltermuster, das in der Shell beim Abgleichen von Dateinamen verwendet wird (auch bekannt als globbing). Wenn Sie jedoch einen Symbolnamen in doppelten Anführungszeichen angeben, wird der Name als Literal und nicht als Glob-Muster behandelt.

Als nächstes definiert das Versionsskript die Knoten VERS_1.2. Dieser Knoten hängt von ab VERS_1.1. Dieses Skript foo2bindet Symbole an Versionsknoten VERS_1.2.

Schließlich definiert das Versionsskript den Knoten VERS_2.0. Dieser Knoten hängt von ab VERS_1.2. Das Skript bindet Symbole bar1und bar2an Versionsknoten VERS_2.0.

Wenn der Linker ein in einer Bibliothek definiertes Symbol findet, das nicht explizit an einen Versionsknoten gebunden ist, bindet er es effektiv an die nicht spezifizierte Basisversion der Bibliothek. Sie können dies irgendwo in Ihrem Versionsskript verwenden, global: *;um alle nicht spezifizierten Symbole an einen bestimmten Versionsknoten zu binden.

Beachten Sie, dass die Verwendung von Platzhaltern in globalen Spezifikationen etwas verrückt ist, mit Ausnahme des Knotens der letzten Version. An anderer Stelle können globale Platzhalter versehentlich Symbole zu Sätzen hinzufügen, die für ältere Versionen exportiert wurden. Das ist falsch, da ältere Versionen einen festen Symbolsatz haben sollten.

Die Namen der Versionsknoten haben keine besondere Bedeutung außer dem, was sie für diejenigen, die sie lesen, implizieren könnten. Versionen können auch zwischen und 2.0erscheinen . Dies wäre jedoch eine verwirrende Art, das Versionsskript zu schreiben.1.11.2

Der Knotenname kann weggelassen werden, wenn es sich um den einzigen Versionsknoten im Versionsskript handelt. Solche Versionierungsskripte weisen Symbolen keine Versionen zu, sondern wählen nur aus, welche Symbole global sichtbar sind und welche nicht.

{ global: foo; bar; local: *; };

Wenn eine Anwendung mit einer gemeinsam genutzten Bibliothek verknüpft wird, die über versionierte Symbole verfügt, weiß die Anwendung selbst, welche Version jedes Symbols sie benötigt, und weiß auch, mit welchem ​​Versionsknoten in jeder gemeinsam genutzten Bibliothek sie verknüpft werden muss.

Daher kann der dynamische Lader zur Laufzeit eine schnelle Überprüfung durchführen, um sicherzustellen, dass die zu verknüpfende Bibliothek tatsächlich alle Versionsknoten bereitstellt, die die Anwendung zum Auflösen aller dynamischen Symbole benötigt. Auf diese Weise weiß der dynamische Linker mit Sicherheit, dass alle benötigten externen Symbole auflösbar sind, ohne nach jeder Symbolreferenz suchen zu müssen.

Nach dem Kompilieren in eine dynamische Bibliothek können Sie readelf --wide -s xxx.soden Symbolstatus der Zielbibliothek anzeigen.

3.4 Definition verschachtelter Funktionen

Die erweiterte gcc-Syntax unterstützt verschachtelte Funktionen, die zu bestimmten Zeiten zum Ersetzen von Makrofunktionen und Hook-Funktionen verwendet werden können, wodurch der Code einfacher zu lesen und zu analysieren ist.

bar (int *array, int offset, int size)
{
    
    
  int access (int *array, int index)
    {
    
     return array[index + offset]; }
  int i;
  /* … */
  for (i = 0; i < size; i++)
    /* … */ access (array, i) /* … */
}

Es ist jedoch sehr wahrscheinlich, dass die IDE die entsprechende Syntax nicht analysieren kann, sodass bestimmte Techniken zum Konvertieren erforderlich sind. Im Folgenden finden Sie eine Definition, die es der IDE ermöglicht, die Syntax wie folgt normal zu analysieren:

/**
 * 对于C语言, 其标准目前还不支持匿名函数, 也就是Lambda函数, 但是gcc扩展语法支持嵌套函数.
 * 为了简化编程逻辑, 降低嵌套复杂宏的使用, 减少代码体积, 方便GDB调试.
 * 因此这里使用了gcc扩展语法支持的C 嵌套函数.
 * 为了避免IDE无法识别C 嵌套函数的语法,因此使用宏转换一下, 只在编译时才会展开.
 * 宏定义会将函数名定义和函数体分开, 便于编写代码时调试和开发.
 *
 * 该扩展语法实现原理和python/C++/JS/Java等语言一致, 即词法作用域(lexical scoping)
 *
 * 嵌套函数相关的代码都可以使用宏函数和回调函数实现, 但从代码间接性和阅读性来说, 嵌套函数更加直观.
 *
 * 参考:
 * https://gcc.gnu.org/onlinedocs/gcc/Nested-Functions.html
 *
 */

/* clang-format off */
#ifndef _NETFPC_COMPILING
/* 让ide不会报错, 可能无法识别嵌套函数语法 */
#define netfpc_lambda(ret, name, arg)   ret (*name)(arg); name = NULL ; for (arg; 0;)
#else
#define netfpc_lambda(ret, name, ...)   ret name(__VA_ARGS__)
#endif
/* clang-format on */

Guess you like

Origin blog.csdn.net/Once_day/article/details/132154372