searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

Linux使用动态库避坑指南

2023-01-13 08:34:06
70
0

直接覆盖so文件导致coredump

如果我们更新so文件不停进程而直接使用cp覆盖so的方式,有概率照成程序coredump。

先看一下用cp更新so的时候发生了什么事情

strace cp testnew.so testold.so

发现老的so被trunc了,在此过程中发生的事情为

  1. 应用程序通过dlopen打开so的时候,kernel通过mmap把so加载到进程地址空间,对应于vma里的几个page。
  2. 在这个过程中loader会把so里面引用的外部符号例如malloc printf等解析成真正的虚存地址。
  3. so被cp覆盖时,确切地说是被trunc时,kernel会把so文件在虚拟内的页purge 掉。
  4. 当运行到so里面的代码时,因为物理内存中不再有实际的数据(仅存在于虚存空间内),会产生一次缺页中断。
  5. kernel从so文件中copy一份到内存中去
  6. 但是这时的全局符号表并没有经过解析,当调用到时就产生segment fault
  7. 如果需要的文件偏移大于新的so的地址范围,就会产生bus error

所以,如果用相同的so去覆盖

  1. 如果so 里面依赖了外部符号,coredump
  2. 如果so里面没有依赖外部符号,运气不错,不会coredump

coredump问题的根源就是因为so被trunc了,所以如果不用trunc的方式就可以避免这个问题。可以使用install命令规避该问题的出现。看看install的时候发生了什么

install 的方式跟cp不同,先unlink再create,当unlink的时候,已经map的虚拟空间vma中的inode结点没有变,只有inode结点的引用计数为0是,kernel才把它干掉.

也就是说so文件的inode号会发生改变,但老的so的inode号依旧存在,这时程序必须停止重启服务才能使用新的so,否则程序继续执行,使用的还是老的so,所以程序不会core掉。

现在我们知道cp so命令会导致运行的进程产生coredump并非是我们程序代码的问题,而是用不当方式升级so造成的,不用耗费精力去分析coredump文件从而来定位是哪里的代码有问题了。

库链接方式导致coredump

在项目中遇到一个问题。可执行文件fs_master_tests链接了fs_master动态库,fs_master库public方式链接了spdk库。执行文件A时报coredump,出错信息如下:

查看spdk的源代码,在tailq.h中定义了EAL_REGISTER_TAILQ

/**

 * Register a tail queue.

 *

 * Register a tail queue from shared memory.

 * This function is mainly used by EAL_REGISTER_TAILQ macro which is used to

 * register tailq from the different dpdk libraries. Since this macro is a

 * constructor, the function has no access to dpdk shared memory, so the

 * registered tailq can not be used before call to rte_eal_init() which calls

 * rte_eal_tailqs_init().

 *

 * @param t

 *   The tailq element which contains the name of the tailq you want to

 *   create (/retrieve when in secondary process).

 * @return

 *   0 on success or -1 in case of an error.

 */

int rte_eal_tailq_register(struct rte_tailq_elem *t);

 

#define EAL_REGISTER_TAILQ(t) \

RTE_INIT(tailqinitfn_ ##t) \

{ \

    if (rte_eal_tailq_register(&t) < 0) \

        rte_panic("Cannot initialize tailq: %s\n", t.name); \

}

在rte_common.h中定义了constructor属性的函数会先于main()执行,DPDK中定义了RTE_INIT()宏,用来声明一个构造函数:

#define RTE_PRIO(prio) \

    RTE_PRIORITY_ ## prio

 

/**

 * Run function before main() with high priority.

 *

 * @param func

 *   Constructor function.

 * @param prio

 *   Priority number must be above 100.

 *   Lowest number is the first to run.

 */

#ifndef RTE_INIT_PRIO /* Allow to override from EAL */

#define RTE_INIT_PRIO(func, prio) \

static void __attribute__((constructor(RTE_PRIO(prio)), used)) func(void)

#endif

 

/**

 * Run function before main() with low priority.

 *

 * The constructor will be run after prioritized constructors.

 *

 * @param func

 *   Constructor function.

 */

#define RTE_INIT(func) \

RTE_INIT_PRIO(func, LAST)

然后定义了很多初始化构造函数,比如pci_common_uio.c中

static struct rte_tailq_elem rte_uio_tailq = {

    .name = "UIO_RESOURCE_LIST",

};

EAL_REGISTER_TAILQ(rte_uio_tailq)

出错原因是涉及到的constructor函数代码有多个实例,被重复调用。那么为什么fs_master_tests中有多个constructor实例呢。我们看看可执行文件fs_master_test的符号表

t表示在本地的代码段中有可执行符号tailqinitfn_rte_uio_tailq

查看fs_master_test的动态符号表中不包含tailqinitfn_rte_uio_tailq

查看fs_mater_test链接的动态库fs_master中本地代码段中含有符号tailqinitfn_rte_uio_tailq

而fs_master的动态符号表中同样没有包含tailqinitfn_rte_uio_tailq符号

当执行fs_mater_test时因为动态加载器在fs_mater_test的动态符号表中没有发现tailqinitfn_rte_uio_tailq符号,所以会将fs_master库中的tailqinitfn_rte_uio_tailq加载到fs_mater_test中,而fs_master_test本身的静态符号表中已有tailqinitfn_rte_uio_tailq,这样就照成tailqinitfn_rte_uio_tailq被执行了两次。问题的根源是fs_master_test能看到spdk库中的dpdk实现,所以对fs_master_test屏蔽掉spdk的dpdk实现就可以修复该问题。将fs_master链接spdk的方式从public改为private。

重新编译以后查看fs_master_test的静态符号表,可以看到在fs_master库中依然还有tailqinitfn_rte_uio_tailq符号。而在fs_master_test中已没有tailqinitfn_rte_uio_tailq符号。运行fs_master_test不再coredump

总结:

CMake中target_link_libraries(A B)中链接动态库的三种方式区别。

  1. PRIVATE 依赖项B仅链接到目标 A,若有C链接了目标A,C不链接依赖项B 。
  2. INTERFACE  依赖项B并不链接到目标 A,若有C链接了目标A,C会链接依赖B 。
  3. PUBLIC 依赖项B链接到目标A,若有C链接了目标A,C也会链接依赖项 B 。

从使用的角度解释,若有 C 链接了目标 A :

  1. 如果依赖项 B 仅用于目标 A 的实现,且不在头文件中提供给 C 使用,使用PRIVATE 。
  2. 如果依赖项 B 不用于目标 A 的实现,仅在头文件中作为接口提供给 C 使用,使用INTERFACE 。
  3. 如果依赖项 B 不仅用于目标 A 的实现,而且在头文件提供给 C 使用,使用PUBLIC
0条评论
0 / 1000
l****n
2文章数
0粉丝数
l****n
2 文章 | 0 粉丝
l****n
2文章数
0粉丝数
l****n
2 文章 | 0 粉丝
原创

Linux使用动态库避坑指南

2023-01-13 08:34:06
70
0

直接覆盖so文件导致coredump

如果我们更新so文件不停进程而直接使用cp覆盖so的方式,有概率照成程序coredump。

先看一下用cp更新so的时候发生了什么事情

strace cp testnew.so testold.so

发现老的so被trunc了,在此过程中发生的事情为

  1. 应用程序通过dlopen打开so的时候,kernel通过mmap把so加载到进程地址空间,对应于vma里的几个page。
  2. 在这个过程中loader会把so里面引用的外部符号例如malloc printf等解析成真正的虚存地址。
  3. so被cp覆盖时,确切地说是被trunc时,kernel会把so文件在虚拟内的页purge 掉。
  4. 当运行到so里面的代码时,因为物理内存中不再有实际的数据(仅存在于虚存空间内),会产生一次缺页中断。
  5. kernel从so文件中copy一份到内存中去
  6. 但是这时的全局符号表并没有经过解析,当调用到时就产生segment fault
  7. 如果需要的文件偏移大于新的so的地址范围,就会产生bus error

所以,如果用相同的so去覆盖

  1. 如果so 里面依赖了外部符号,coredump
  2. 如果so里面没有依赖外部符号,运气不错,不会coredump

coredump问题的根源就是因为so被trunc了,所以如果不用trunc的方式就可以避免这个问题。可以使用install命令规避该问题的出现。看看install的时候发生了什么

install 的方式跟cp不同,先unlink再create,当unlink的时候,已经map的虚拟空间vma中的inode结点没有变,只有inode结点的引用计数为0是,kernel才把它干掉.

也就是说so文件的inode号会发生改变,但老的so的inode号依旧存在,这时程序必须停止重启服务才能使用新的so,否则程序继续执行,使用的还是老的so,所以程序不会core掉。

现在我们知道cp so命令会导致运行的进程产生coredump并非是我们程序代码的问题,而是用不当方式升级so造成的,不用耗费精力去分析coredump文件从而来定位是哪里的代码有问题了。

库链接方式导致coredump

在项目中遇到一个问题。可执行文件fs_master_tests链接了fs_master动态库,fs_master库public方式链接了spdk库。执行文件A时报coredump,出错信息如下:

查看spdk的源代码,在tailq.h中定义了EAL_REGISTER_TAILQ

/**

 * Register a tail queue.

 *

 * Register a tail queue from shared memory.

 * This function is mainly used by EAL_REGISTER_TAILQ macro which is used to

 * register tailq from the different dpdk libraries. Since this macro is a

 * constructor, the function has no access to dpdk shared memory, so the

 * registered tailq can not be used before call to rte_eal_init() which calls

 * rte_eal_tailqs_init().

 *

 * @param t

 *   The tailq element which contains the name of the tailq you want to

 *   create (/retrieve when in secondary process).

 * @return

 *   0 on success or -1 in case of an error.

 */

int rte_eal_tailq_register(struct rte_tailq_elem *t);

 

#define EAL_REGISTER_TAILQ(t) \

RTE_INIT(tailqinitfn_ ##t) \

{ \

    if (rte_eal_tailq_register(&t) < 0) \

        rte_panic("Cannot initialize tailq: %s\n", t.name); \

}

在rte_common.h中定义了constructor属性的函数会先于main()执行,DPDK中定义了RTE_INIT()宏,用来声明一个构造函数:

#define RTE_PRIO(prio) \

    RTE_PRIORITY_ ## prio

 

/**

 * Run function before main() with high priority.

 *

 * @param func

 *   Constructor function.

 * @param prio

 *   Priority number must be above 100.

 *   Lowest number is the first to run.

 */

#ifndef RTE_INIT_PRIO /* Allow to override from EAL */

#define RTE_INIT_PRIO(func, prio) \

static void __attribute__((constructor(RTE_PRIO(prio)), used)) func(void)

#endif

 

/**

 * Run function before main() with low priority.

 *

 * The constructor will be run after prioritized constructors.

 *

 * @param func

 *   Constructor function.

 */

#define RTE_INIT(func) \

RTE_INIT_PRIO(func, LAST)

然后定义了很多初始化构造函数,比如pci_common_uio.c中

static struct rte_tailq_elem rte_uio_tailq = {

    .name = "UIO_RESOURCE_LIST",

};

EAL_REGISTER_TAILQ(rte_uio_tailq)

出错原因是涉及到的constructor函数代码有多个实例,被重复调用。那么为什么fs_master_tests中有多个constructor实例呢。我们看看可执行文件fs_master_test的符号表

t表示在本地的代码段中有可执行符号tailqinitfn_rte_uio_tailq

查看fs_master_test的动态符号表中不包含tailqinitfn_rte_uio_tailq

查看fs_mater_test链接的动态库fs_master中本地代码段中含有符号tailqinitfn_rte_uio_tailq

而fs_master的动态符号表中同样没有包含tailqinitfn_rte_uio_tailq符号

当执行fs_mater_test时因为动态加载器在fs_mater_test的动态符号表中没有发现tailqinitfn_rte_uio_tailq符号,所以会将fs_master库中的tailqinitfn_rte_uio_tailq加载到fs_mater_test中,而fs_master_test本身的静态符号表中已有tailqinitfn_rte_uio_tailq,这样就照成tailqinitfn_rte_uio_tailq被执行了两次。问题的根源是fs_master_test能看到spdk库中的dpdk实现,所以对fs_master_test屏蔽掉spdk的dpdk实现就可以修复该问题。将fs_master链接spdk的方式从public改为private。

重新编译以后查看fs_master_test的静态符号表,可以看到在fs_master库中依然还有tailqinitfn_rte_uio_tailq符号。而在fs_master_test中已没有tailqinitfn_rte_uio_tailq符号。运行fs_master_test不再coredump

总结:

CMake中target_link_libraries(A B)中链接动态库的三种方式区别。

  1. PRIVATE 依赖项B仅链接到目标 A,若有C链接了目标A,C不链接依赖项B 。
  2. INTERFACE  依赖项B并不链接到目标 A,若有C链接了目标A,C会链接依赖B 。
  3. PUBLIC 依赖项B链接到目标A,若有C链接了目标A,C也会链接依赖项 B 。

从使用的角度解释,若有 C 链接了目标 A :

  1. 如果依赖项 B 仅用于目标 A 的实现,且不在头文件中提供给 C 使用,使用PRIVATE 。
  2. 如果依赖项 B 不用于目标 A 的实现,仅在头文件中作为接口提供给 C 使用,使用INTERFACE 。
  3. 如果依赖项 B 不仅用于目标 A 的实现,而且在头文件提供给 C 使用,使用PUBLIC
文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0