C (8) marco de compilación de biblioteca dinámica de Linux

C (8) Marco básico de compilación de biblioteca dinámica de Linux

Autor: Una vez al día Fecha: 5 de agosto de 2023

Ha sido un largo camino, pero alguien te ha sonreído...

Documentos citados de referencia:

1. Información general

Muchas veces necesitamos desarrollar un conjunto de bibliotecas básicas para el desarrollo de programas. Generalmente hay dos formas de hacer esto: una es la distribución del código fuente y la otra es la distribución de archivos de la biblioteca. Lo que voy a presentar a continuación es el método de distribución de archivos de la biblioteca. La plataforma de destino es un sistema Linux. Aunque los detalles de los diferentes sistemas son diferentes, la idea general es similar.

Todo el marco de compilación de la biblioteca dinámica se divide en tres partes:

  1. Los archivos fuente básicos se utilizan para construir una capa de protección de interfaz abstracta, dejando cierta base para futuros trasplantes entre sistemas.
  2. Script de compilación, cómo compilar y generar resultados, cómo agregar las opciones de compilación necesarias.
  3. El script de prueba se integra con el marco de compilación general y genera la información requerida.

Todo el marco actual es todavía relativamente rudimentario, pero lo importante es todo el proceso de aprendizaje y la comprensión de los conocimientos y conceptos relevantes.

Generalmente, todo el marco del proyecto se puede dividir en las siguientes partes:

── 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 // 通用工具代码
......(编译+脚本+说明+许可证+日志+...等信息)

Lo anterior es una estructura de directorio de desarrollo de software común. Cada software será diferente, pero la idea general es similar. El punto es que cada sección debe colocarse en una carpeta separada para que sea más fácil de mantener.

2. Conceptos básicos de compilación

2.1 Dependencias de compilación

En comparación con simplemente escribir una pieza de software, crear una biblioteca de software de código abierto estándar requiere un análisis estricto de las dependencias de compilación. El énfasis aquí está en la biblioteca glibc, porque otras bibliotecas dependen de manera muy explícita.

En términos generales, el compilador vendrá con algunos archivos de biblioteca , que son los archivos de encabezado más originales, pero stdio.hobviamente no están entre ellos. La relación de dependencia más básica es que una vez que utiliza stdio.hun archivo de encabezado C estándar similar, debe confiar en la biblioteca libc. Esto todavía no es muy exacto. Por ejemplo, la base de datos libm.so y la biblioteca de subprocesos libpthread.so son dependencias adicionales. Sobre esta base, se deben distinguir algunas funciones de extensión y sintaxis de GNU.

El primero es la dependencia del estándar Posix: la compilación predeterminada tiene la escalabilidad más baja, solo admite el estándar C y la portabilidad más alta, con definición XOPEN_SOURCEy _GNU_SOURCEcontrol generales.

_XOPEN_SOURCEes una macro que se puede definir al compilar un programa C o C++. Esta macro se utiliza para habilitar la funcionalidad definida en los estándares X/Open y POSIX, incluidas muchas llamadas al sistema y funciones de biblioteca de Unix y Linux.

Dependiendo del _XOPEN_SOURCEvalor definido de , se pueden habilitar funciones en diferentes versiones de los estándares X/Open y POSIX. Por ejemplo:

  • Si se define como 500, se habilita la funcionalidad compatible con SUSv2 (especificación única de UNIX versión 2) y POSIX.1 1996.
  • Si se define como 600, se habilitan las funciones compatibles con SUSv3 (especificación única de UNIX versión 3) y POSIX.1 2001.
  • Si se define como 700, se habilitan las funciones compatibles con SUSv4 (especificación única de UNIX versión 4) y POSIX.1 2008.

Normalmente, esta macro se define en la línea de comando del compilador o mediante una directiva de preprocesador al comienzo del código fuente, de la siguiente manera:

#define _XOPEN_SOURCE 700

Definir esta macro puede ayudar a garantizar que su código sea portátil en diferentes sistemas similares a Unix porque garantiza que su código solo utilice la funcionalidad definida en los estándares X/Open y POSIX.

De hecho, se puede definir directamente CFLAGS += -D_GNU_SOURCEpara admitir todas las extensiones de funciones de GNU en la mayor medida y mejorar el soporte para operaciones atómicas/bibliotecas de subprocesos/archivos IO/funciones de cadena. El software desarrollado normalmente no tiene requisitos de trasplante tan altos. Implementar estas funciones básicas equivale a reinventar la rueda. A menos que sea necesario, suele ser mejor reutilizarlas directamente.

2.2 versión en lenguaje C

En términos generales, no le importa demasiado la versión en lenguaje C, que es básicamente C99, pero hay un malentendido. Porque, en términos generales, la sintaxis extendida de gcc está cubierta, de hecho, la sintaxis estándar pura de C99 no puede satisfacer ciertas necesidades, como estructuras/uniones anónimas. Por lo tanto, actualmente se recomienda utilizar C11 directamente como estándar básico.

Luego habilite suficientes opciones de compilación. Las siguientes son opciones de compilación comunes:

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

Para escenarios de compilación comunes, las opciones anteriores son suficientes para cubrir, pero esto no es suficiente. Necesitamos habilitar más opciones adicionales, de la siguiente manera.

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

De hecho, -Wall -Wextra -Werrorlas tres opciones permitirán una gran cantidad de verificaciones de compilación. La mayoría de las opciones del compilador anteriores son otras opciones de verificación que están excluidas . Activar tantas comprobaciones hará que cada paso del código sea difícil de escribir, porque el código C puede ser realmente arbitrario, como punteros y conversiones de tipos, declaraciones de variables, etc. Los estrictos requisitos de este tipo de código son una especie de impulso de autodisciplina: sólo cultivando hábitos estrictos se puede mejorar la calidad del código.

Además, para imprimir funciones con parámetros variables, es necesario verificar la correspondencia entre la cadena de formato y el tipo de parámetro, de la siguiente manera:

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

__attribute__((format(printf, 3, 4)))El tercer parámetro de la función especificada es una cadena de formato y el cuarto parámetro es el comienzo de la lista de parámetros de esta cadena de formato. El compilador comprobará my_printfsi el uso del tercer parámetro al llamar se ajusta printfa las reglas de formato de cadena de , por ejemplo:

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

Esta propiedad es muy útil porque puede ayudar a detectar algunos problemas comunes causados ​​por el uso incorrecto de cadenas de formato. Aunque no forma parte del estándar C/C++, es ampliamente compatible con GCC y algunos otros compiladores compatibles.

2.3 especificación de atributos del compilador gcc

Además de la sintaxis normal de C, también se necesitan algunos métodos para comunicarse y operar con el compilador, como se muestra a continuación:

#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错误 */

Aquí, a través de #pragmainstrucciones, se cambian brevemente los parámetros verificados por el compilador para evitar errores en el informe, mediante este tipo de ajuste sutil se pueden controlar mejor los resultados de la compilación.

Aquí hay algunos otros controles de propiedad:

  • atributo ((constructor)) y atributo ((destructor)): Estos dos atributos se pueden usar para especificar que la función se llama automáticamente antes de que el programa comience la ejecución (constructor) o después de que finalice la ejecución (destructor).
  • atributo ((empaquetado)): este atributo se utiliza para controlar el diseño de la memoria de la estructura para que sea lo más pequeña posible. Normalmente, el compilador alineará los campos de una estructura para optimizar la velocidad de acceso, lo que puede resultar en el uso de espacio de memoria adicional. El atributo empaquetado le dice al compilador que no alinee esta estructura.
  • atributo ((aligned(n))): este atributo se utiliza para especificar los requisitos mínimos de alineación para variables o estructuras. Por ejemplo, el atributo ((aligned(16))) garantizará que la dirección del objeto en la memoria sea múltiplo de 16.
  • atributo ((noreturn)): este atributo se utiliza para especificar que la función nunca volverá. Esto es útil para funciones como salir o cancelar, porque el compilador puede realizar optimizaciones especiales para dichas funciones.
  • atributo ((obsoleto)): este atributo se utiliza para marcar funciones o variables que están en desuso. El compilador generará una advertencia si intenta utilizar una función o variable marcada con el atributo obsoleto.
  • atributo ((no utilizado)): se utiliza para especificar que una función, parámetro de función o variable no se puede utilizar. Sin este atributo, el compilador puede generar una advertencia, ya que los parámetros o variables no utilizados suelen indicar un error de programación.
  • atributo ((hot)) y atributo ((cold)) : estos dos atributos proporcionan al compilador una pista sobre la frecuencia con la que se ejecuta una función. hotLas propiedades indican que la función se llama con frecuencia ycoldlas propiedades indican que la función rara vez se llama. Esta información ayuda al compilador a decidir cómo optimizar el código. Por ejemplo, el compilador puede colocar funciones llamadas con frecuencia juntas para aprovechar el caché de la CPU, mientras que las funciones llamadas con poca frecuencia pueden colocarse en una ubicación distante del programa.
  • atributo ((alias)) : este atributo puede definir alias para funciones. Por ejemplo, si tiene una funciónoriginal_function, puede definir un alias para ella.alias_function

Hay muchos atributos similares. Para obtener más detalles, consulte la documentación de gcc (hay un enlace al documento de referencia en la parte superior).

2.4 Operaciones relacionadas con la energía atómica

Las variables de tipo atómico generalmente se volatilemodifican mediante modificadores, pero generalmente deben implementarlas usted mismo, atomic.hsolo use archivos.

Las funciones atómicas admitidas por gcc se dividen en dos tipos, una es la antigua función de extensión GCC y la otra es la nueva función del estándar C11.

Funciones como la siguiente __sync_xxxx son extensiones GNU antiguas, compatibles con GCC 4.1.2 :

__sync_fetch_and_add, __sync_fetch_and_sub, __sync_fetch_and_or,__sync_fetch_and_and, __sync_fetch_and_xor, __sync_fetch_and_nand

Este tipo de función primero devuelve el valor anterior de la variable y luego realiza una operación específica en la variable. Por ejemplo, __sync_fetch_and_adduna función devuelve el valor anterior de una variable y luego agrega la variable.

__sync_add_and_fetch, __sync_sub_and_fetch, __sync_or_and_fetch, __sync_and_and_fetch, __sync_xor_and_fetch, __sync_nand_and_fetch

Este tipo de función primero realiza una operación específica en una variable y luego devuelve el valor después de la operación. Por ejemplo, __sync_add_and_fetchuna función agrega una variable y devuelve el valor resultante.

__sync_bool_compare_and_swap, __sync_val_compare_and_swap

Estas dos funciones se utilizan para realizar operaciones de comparación e intercambio. __sync_bool_compare_and_swapDevuelve un valor booleano que indica si la operación fue exitosa. __sync_val_compare_and_swapDevuelve el valor anterior de una variable.

__sync_lock_test_and_set, __sync_lock_release

Estas dos funciones se utilizan para implementar bloqueos simples. __sync_lock_test_and_setEstablecerá un bloqueo o devolverá un valor distinto de cero si el bloqueo ya está establecido.

GCC 4.9.0 comenzó a admitir funciones __atomic_xxxx, como __atomic_fetch_add, etc. Como parte de C11, las nuevas funciones son más compatibles que las funciones antiguas .

  • atomic_init: Esta función se utiliza para inicializar un objeto atómico. Por ejemplo:

    atomic_int ai;
    atomic_init(&ai, 0);
    
  • atomic_store: Esta función almacena un valor de forma atómica. Es decir, en un entorno de subprocesos múltiples, la operación no será interrumpida por otros subprocesos durante la ejecución. Por ejemplo:

    atomic_int ai;
    atomic_store(&ai, 10);
    
  • atomic_load: Esta función carga un valor de forma atómica. Al atomic_storeigual que esta operación es atómica. Por ejemplo:

    atomic_int ai = ATOMIC_VAR_INIT(10);
    int i = atomic_load(&ai);
    
  • atomic_fetch_addSuma atomic_fetch_sub: estas dos funciones realizan operaciones de suma y resta de forma atómica y devuelven el valor antes de la operación. Por ejemplo:

    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: Esta función intenta comparar e intercambiar valores de forma atómica. Si el valor actual del objeto atómico es igual al valor esperado, el valor del objeto se establece en el valor deseado y se devuelve verdadero; de lo contrario, el valor esperado se establece en el valor actual del objeto y es falso. es regresado. Por ejemplo:

    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: Realice atómicamente una operación OR bit a bit (es decir, "OR bit a bit") y devuelva el valor antes de la operación. Por ejemplo:

    atomic_int ai = ATOMIC_VAR_INIT(0b0011);
    int i = atomic_fetch_or(&ai, 0b0101); // i 现在是 0b0011,ai 现在是 0b0111
    
  • atomic_fetch_xor: Realice atómicamente una operación XOR bit a bit (es decir, "bit XOR") y devuelva el valor antes de la operación. Por ejemplo:

    atomic_int ai = ATOMIC_VAR_INIT(0b0011);
    int i = atomic_fetch_xor(&ai, 0b0101); // i 现在是 0b0011,ai 现在是 0b0110
    
  • atomic_fetch_and: Realiza atómicamente una operación AND bit a bit (es decir, "Y bit a bit") y devuelve el valor antes de la operación. Por ejemplo:

    atomic_int ai = ATOMIC_VAR_INIT(0b0011);
    int i = atomic_fetch_and(&ai, 0b0101); // i 现在是 0b0011,ai 现在是 0b0001
    
  • atomic_flag_test_and_set: Esta es una atomic_flagfunción especial para el tipo. Se establece atómicamente atomic_flagen verdadero y devuelve su valor anterior. Por ejemplo:

    atomic_flag af = ATOMIC_FLAG_INIT;
    bool prev = atomic_flag_test_and_set(&af); // prev 是 false,af 现在是 true
    
  • atomic_flag_clear: Esta también es una atomic_flagfunción especial para el tipo. Se establece atómicamente atomic_flagen falso. Por ejemplo:

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

En C11, puede especificar un "orden de memoria"<stdatomic.h> para operaciones atómicas en . Esto determina cómo se ordenan los accesos a la memoria (es decir, en qué orden son visibles para otros subprocesos) .

  • memory_order_relaxed( __ATOMIC_RELAXED): Este es el orden de memoria más débil. No proporciona ninguna garantía de sincronización o pedido. Sólo se garantiza que la operación atómica en sí no provocará carreras de datos.
  • memory_order_consume( __ATOMIC_CONSUME): Este orden de memoria garantiza que todas las operaciones de lectura posteriores basadas en el resultado de esta operación solo verán el estado de la memoria antes de esta operación. Sin embargo, no garantiza que otros hilos vean los resultados de esta operación.
  • memory_order_acquire( __ATOMIC_ACQUIRE): Este orden de memoria garantiza que todas las operaciones de lectura y escritura posteriores a esta operación solo verán el estado de la memoria antes de esta operación. Esta es una primitiva de sincronización común que se utiliza para proteger la entrada de una sección crítica.
  • memory_order_release( ): Este orden de memoria garantiza que esta operación y todas las operaciones de lectura y escritura anteriores se completarán antes de cualquier operación __ATOMIC_RELEASEposterior . Esta es una primitiva de sincronización común que se utiliza para proteger la salida de una sección crítica.memory_order_acquirememory_order_consume
  • memory_order_acq_rel( __ATOMIC_ACQ_REL): Esta secuencia de memoria es una combinación de memory_order_acquirey memory_order_release. Garantiza que esta operación y todas las operaciones anteriores se completarán antes de cualquier operación posterior basada en los resultados de esta operación.
  • memory_order_seq_cst( __ATOMIC_SEQ_CST): Este es el orden de memoria más fuerte. Proporciona coherencia secuencial, lo que significa que todos los subprocesos ven el mismo orden de operaciones.

SEQ_CSTEn C11, se pueden lograr operaciones concurrentes más eficientes a través de los diferentes órdenes de memoria mencionados anteriormente, pero el riesgo es muy alto, por lo que las funciones atómicas anteriores se ejecutan en el orden más estricto de forma predeterminada .

Además, también puedes establecer una barrera de memoria completa:

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

En el desarrollo inicial del código de prueba y la operación diaria, para evitar problemas comerciales graves, cuando se detecta un error grave, el programa debe finalizarse directamente en lugar de continuar ejecutándose. Generalmente, la función de aborto se utiliza para finalizar activamente la operación y la capa inferior envía la señal correspondiente SIGABRT a su propio hilo.

El proceso de juzgar se llama aserción y se ejecuta en dos períodos diferentes, el período de compilación y el período de ejecución. Las afirmaciones en tiempo de compilación determinan principalmente si algunas estructuras de datos y macros son normales, mientras que el tiempo de ejecución determina principalmente si cierta información de datos cumple con las expectativas.

Las funciones de aserción en tiempo de compilación generalmente las puede escribir usted mismo, como se define a continuación (hay muchas definiciones de métodos de escritura, puede elegirlas usted mismo):

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

Esto es para afirmar que se debe establecer el objetivo (es decir, se informará un error si no se establece). Alguna lógica es afirmar que se establece la condición y se informará un error. Esta parte se puede configurar de acuerdo con tus propios hábitos.

Las aserciones en tiempo de ejecución pueden tomar prestado el archivo de encabezado <assert.h> en la biblioteca estándar, que ya ha encapsulado una serie de funciones de aserción.

/* 符合C标准的编译器断言检测函数 */
#include <assert.h>
#define xxx_assert(cond)       assert(cond)
#define xxx_assert_perror(str) assert_perror(str)
#define xxx_abort() abort()
2.6 Opciones de compilación de la biblioteca

En Linux, las bibliotecas dinámicas suelen seguir una estrategia de control de versiones denominada "mecanismo de versiones de tres niveles". Esta estrategia se implementa a través de nombres de archivos. Específicamente, el nombre completo de una biblioteca generalmente se ve así: libname.so.major.minor.patch, donde:

  • libnamees el nombre base de la biblioteca.
  • majores el número de versión principal. El número de versión principal aumenta cuando se producen cambios incompatibles en la interfaz pública de la biblioteca.
  • minorEste es el número de versión menor. Cuando se agregan nuevas funciones manteniendo la compatibilidad con versiones anteriores, se incrementa el número de versión secundaria.
  • patches el número de revisión. Cuando se realizan correcciones de errores compatibles con versiones anteriores, se incrementa el número de revisión.

En esta estrategia de control de versiones, hay dos enlaces simbólicos importantes:

  1. libname.so: Esto es para compiladores y enlazadores, y generalmente enlaza a la última versión de la biblioteca utilizada en el desarrollo. Los programas están vinculados a este archivo cuando se compilan.
  2. libname.so.major: Esto es para uso de enlazadores en tiempo de ejecución (como ld.soo ld-linux.so), que generalmente enlazan con la última versión de la biblioteca que es compatible con binarios. El programa se vincula a este archivo cuando se ejecuta.

Este mecanismo permite a los desarrolladores y usuarios instalar y utilizar diferentes versiones de una biblioteca simultáneamente sin interferir entre sí. Por ejemplo, un programa puede ser utilizado por libname.so.1.0.0otro programa libname.so.2.0.0mientras se ejecuta en el mismo sistema al mismo tiempo, siempre que tengan diferentes números de versión principal.

El archivo MAKE generalmente correspondiente es el siguiente:

# 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)Se utiliza para encontrar el nombre del enlace externo de la biblioteca especificada. Después de la compilación, el nombre del archivo generado es el siguiente:

libmyapp.so.2.2.2

Luego necesita generar archivos de símbolos de enlace suave adicionales:

# 安装动态库文件, 用于在目标设备上运行
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)

El formato final es el siguiente, que es el formato de distribución de archivos de biblioteca dinámica estándar de Linux:

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

Hay muchos detalles en todo el Makefile, que no entraré en detalles aquí, y se enumeran a continuación:

###
 # @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 Compatibilidad con la ejecución de scripts

De forma predeterminada, Linux no genera archivos principales y la ruta de la biblioteca dinámica también debe modificarse en consecuencia, por lo que debe utilizar un script para generar un entorno adecuado antes de ejecutar el archivo de prueba, de la siguiente manera:

###
 # @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. Referencia de habilidades de Glibc

3.1 Ocultación de símbolos internos
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)

Lo anterior es un fragmento de texto en el código fuente de glibc, que revela un método de ocultación de símbolos. Nuestro programa no requiere una implementación tan complicada, por lo que podemos extraer parte de él para crear un marco de ocultación de símbolos simple. como sigue:

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

Puede establecer una visibilidad diferente para los símbolos del programa, que se reflejarán en el archivo binario. Usando readelf, puede ver que diferentes símbolos tienen diferentes áreas visibles. Los siguientes tipos:

  • default: Esta es la visibilidad predeterminada del símbolo. En este caso, otros módulos (como bibliotecas compartidas) pueden acceder al símbolo y hacer referencia a él en el momento del enlace. Esto equivale a no especificar un atributo de visibilidad.
  • hidden: Esto significa que el símbolo está oculto en el momento del enlace y otros módulos no accederán ni harán referencia a él. Esto ayuda a lograr la encapsulación y ocultar los detalles de implementación. Tenga en cuenta que incluso si un símbolo se declara externo, se puede ocultar utilizando este atributo.
  • protected: Esto indica que el símbolo tiene visibilidad protegida en el momento del enlace. Esto significa que otros módulos pueden acceder al símbolo, pero no se puede sobrescribir. Esta propiedad es útil al crear bibliotecas compartidas para garantizar que los símbolos de la biblioteca no se sobrescriban con símbolos de la aplicación u otras bibliotecas.
  • internal: Esto indica que el símbolo tiene visibilidad de enlace interno. Esto significa que los símbolos sólo son visibles dentro del módulo actual y se descartan en el momento del enlace. Este atributo se puede utilizar con fines de optimización, por ejemplo, para reducir el código generado cuando el compilador puede determinar que el símbolo solo se usa en el archivo fuente actual.

El segundo es generar nombres de enlaces a nivel de ensamblado:

/**
 * 隐藏的汇编链接符号名, __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

Para diferentes plataformas, los símbolos de función a nivel de ensamblado pueden llevar un símbolo de prefijo, que __USER_LABEL_PREFIX__está definido, generalmente vacío, y que es equivalente al nombre de la función definido en el código fuente C.

En resumen, oculta la visibilidad de los símbolos del código fuente C original y cambia los nombres de sus símbolos de nivel de ensamblaje, de modo que los símbolos puedan protegerse desde el exterior y puedan llamarse internamente.

/**
 * 隐藏内部函数, 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)

Esta xxx_hidden_protoes la definición de declaración del archivo de encabezado final, que restringe la visibilidad del símbolo C y proporciona un nuevo nombre de símbolo de ensamblaje.

Luego redefina un símbolo usado internamente en el archivo fuente C, de la siguiente manera:

/**
 * 重定义原有符号和汇编符号的关系, 使得外部函数可以调用内部函数.
 * 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)

Una cosa a tener en cuenta aquí es que xxx_hidden_proto(func)el archivo de encabezado donde se encuentra la definición es invisible para los archivos externos. De hecho, la definición interna y la definición externa están separadas y luego son equivalentes en el nivel de ensamblaje subyacente. Aquí hay un resumen:

  • El módulo interno contiene xxx_hidden_proto(func)el archivo de encabezado de definición, por lo que en el módulo interno, func es una función interna oculta y el nombre del enlace real es __NI__func.
  • Los módulos externos no contienen xxx_hidden_proto(func)archivos de encabezado de definición, por lo que los módulos externos intentarán vincular funcfunciones.
  • En el módulo interno se define un símbolo __EI_func, que es __NI_funcun alias de, y su símbolo ensamblador es func, por lo que la función se vinculará externamente __EI_funcpara realizar funcla llamada a.

funcComo se puede ver en este paso, el nombre específico en el archivo de encabezado externo y el nombre de la función interna real pueden ser completamente diferentes, por lo que la diferencia entre los dos se puede ocultar fácilmente.

3.2 Control de versión del símbolo de función

Dividido en dos situaciones: versión predeterminada y versión especificada (sintaxis del vinculador GNU para especificar versiones de símbolos):

  • nombre@versión: esta sintaxis se utiliza para definir una versión específica de un símbolo. Cuando el vinculador encuentra esta sintaxis, asocia esta versión del nombre del símbolo con la versión proporcionada. De esta manera, cuando otro código haga referencia a este símbolo, podrá solicitar explícitamente esta versión específica. Tenga en cuenta que si hay varias versiones de una definición de símbolo, el uso de la sintaxis nombre@versión hace que el vinculador seleccione una versión específica.
  • nombre@@versión: esta sintaxis se utiliza para definir la versión predeterminada de un símbolo. Cuando el vinculador encuentra esta sintaxis, asocia la versión predeterminada del nombre del símbolo con la versión proporcionada. Esto significa que cuando otro código haga referencia a este símbolo, el vinculador elegirá esta versión predeterminada si no se solicita explícitamente una versión específica. Un símbolo solo puede tener una versión predeterminada, use nombre@@versión
/**
 * 导出函数带版本定义,用于区分不同版本的接口:
 *  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);

Por lo general, esto no se usa mucho, pero se puede usar en caso de emergencia. Los detalles relevantes deben comprenderse en los detalles del trabajo de LD.

Después de compilar en una biblioteca dinámica, puede readelf --wide -s xxx.sover el estado del símbolo de la biblioteca de destino.

3.3 Script de versión de enlace LD

Además de configurar manualmente la visibilidad y la versión del símbolo mediante las propiedades del compilador. La visibilidad de los símbolos externos también se puede controlar a través de los scripts del vinculador LD.

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

version.s es un archivo de script de control de versiones especialmente compilado, como se muestra a continuación:

# 库版本控制脚本, 详细信息参考以下文档:
# 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;

El script de versión define un árbol de nodos de versión. Especifique los nombres de los nodos y las interdependencias en el script de versión. Es posible especificar qué símbolos están vinculados a qué nodos de versión, y el conjunto de símbolos especificado se puede reducir a un ámbito local para que no sean visibles globalmente fuera de la biblioteca compartida.

Este script de versión de ejemplo define tres nodos de versión. El primer nodo de versión definido es VERS_1.1; no tiene otras dependencias. El script foo1vincula el símbolo a VERS_1.1. Reduce algunos símbolos al ámbito local para que no sean visibles fuera de la biblioteca compartida; esto se hace usando un patrón de comodines, por lo que cualquier símbolo cuyo nombre comience con oldo originalcoincidirá new. Poder

El patrón comodín utilizado es el mismo que el patrón comodín utilizado en el shell al hacer coincidir nombres de archivos (también conocido como globbing). Sin embargo, si especifica el nombre de un símbolo entre comillas dobles, el nombre se trata como un patrón literal, no global.

A continuación, el script de versión define los nodos VERS_1.2. Este nodo depende de VERS_1.1. Este script foo2vincula símbolos a nodos de versión VERS_1.2.

Finalmente, el script de versión define el nodo VERS_2.0. Este nodo depende de VERS_1.2. El script vincula símbolos bar1y bar2nodos de versión VERS_2.0.

Cuando el vinculador encuentra un símbolo definido en una biblioteca que no está vinculado explícitamente a un nodo de versión, lo vincula efectivamente a la versión base no especificada de la biblioteca. Puede usar esto en algún lugar del script de su versión global: *;para vincular todos los símbolos no especificados a un nodo de versión determinado.

Tenga en cuenta que usar comodines en las especificaciones globales es un poco loco, excepto en el nodo de la última versión. En otros lugares, los comodines globales pueden agregar accidentalmente símbolos a conjuntos exportados para versiones anteriores. Esto es incorrecto porque las versiones anteriores deberían tener un conjunto fijo de símbolos.

Los nombres de los nodos de versión no tienen ningún significado específico distinto del que podrían implicar para quienes los leen. 2.0También pueden aparecer versiones entre 1.1y 1.2. Sin embargo, esta sería una forma confusa de escribir el script de versión.

El nombre del nodo se puede omitir si es el único nodo de versión en el script de versión. Estos scripts de control de versiones no asignan ninguna versión a los símbolos, solo seleccionan qué símbolos serán visibles globalmente y cuáles no.

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

Cuando una aplicación está vinculada a una biblioteca compartida que tiene símbolos versionados, la aplicación misma sabe qué versión de cada símbolo necesita y también sabe con qué nodo de versión en cada biblioteca compartida necesita vincularse.

Por lo tanto, en tiempo de ejecución, el cargador dinámico puede realizar una verificación rápida para garantizar que la biblioteca que se vincula realmente proporcione todos los nodos de versión requeridos por la aplicación para resolver todos los símbolos dinámicos. De esta manera, el vinculador dinámico sabe con certeza que todos los símbolos externos que necesita se pueden resolver sin tener que buscar cada referencia de símbolo.

Después de compilar en una biblioteca dinámica, puede readelf --wide -s xxx.sover el estado del símbolo de la biblioteca de destino.

3.4 Definición de función anidada

La sintaxis extendida de gcc admite funciones anidadas, que se pueden usar para reemplazar funciones macro y funciones de enlace en ciertos momentos, lo que hace que el código sea más fácil de leer y analizar.

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) /* … */
}

Sin embargo, es muy probable que el IDE no pueda analizar la sintaxis correspondiente, por lo que se necesitan ciertas técnicas para convertirla. La siguiente es una definición que permite al IDE analizar la sintaxis normalmente, de la siguiente manera:

/**
 * 对于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 */

Supongo que te gusta

Origin blog.csdn.net/Once_day/article/details/132154372
Recomendado
Clasificación