type
Post
status
Published
date
Feb 28, 2025
slug
2025/02/28/Why-are-there-multiple-implementations-of-the-same-function-in-Linux-and-What-is-the-strength-of-symbols
summary
tags
Linux
category
学习思考
created days
new update day
icon
password
Created_time
Mar 1, 2025 01:43 AM
Last edited time
Mar 1, 2025 01:53 AM

Linux 中为什么会存在同一个函数的多重定义?

最新在分析一个内核模块的功能的时候,使用 clangd 与 bear 配合查找函数的引用的时候,发现了一个很神奇的问题。
在驱动的 probe 函数中,有一个 pci_enable_device(pci_dev); 函数的调用
///初始化设备,使得I/O, memory可用,唤醒设备 ret = pci_enable_device(pci_dev); if (ret) { printk(KERN_ERR "Cannot enable PCI device, aborting.\n"); goto err_out_free_dev; }
点进去追踪之后发现在 /usr/src/linux-headers-`uname -r`/inlcude/linux/pci.h 中发现了对应函数的声明。
int __must_check pci_enable_device(struct pci_dev *dev);
再点进去之后,发现在 pci.h 文件中,拥有这个函数的一个实现,不过是静态内联函数。
#define EIO 5 /* I/O error */ static inline int pci_enable_device(struct pci_dev *dev) { return -EIO; }
这个时候你可能和我一样,发现这个函数的返回值好像是负数啊,那么,我在上面的函数调用不就是一直失败的嘛。

查看内核源码

为了一探究竟,我去网上搜索查看了对应的内核源码。通过搜索发现,与之相关联的定义与实现存在 2 个文件中
notion image
通过观察这3个定义,这个时候我发现了对应的端倪。
下面是 pci.c 文件中的定义,可以发现,这个才应该是最正确的实现。那这个时候我就有疑问了。
💡
那么既然这个才是实际的有效实现,那么另一个静态内联函数是干什么的呢?
💡
一个函数有多重定义那么为什么编译不会报错呢?我自己编写的时候每次都会报错。
/** * pci_enable_device - Initialize device before it's used by a driver. * @dev: PCI device to be initialized * * Initialize device before it's used by a driver. Ask low-level code * to enable I/O and memory. Wake up the device if it was suspended. * Beware, this function can fail. * * Note we don't actually enable the device many times if we call * this function repeatedly (we just increment the count). */ int pci_enable_device(struct pci_dev *dev) { return pci_enable_device_flags(dev, IORESOURCE_MEM | IORESOURCE_IO); } EXPORT_SYMBOL(pci_enable_device);
这种设计看起来很奇怪,但实际上它是 Linux 内核中一种常见的机制,用于处理 条件编译 和 模块化设计

原因分析

1. 条件编译机制

在 include/linux/pci.h 中,pci_enable_device 的定义可能是为了处理某些特殊情况,例如:
  • PCI 子系统未启用:如果内核编译时未启用 PCI 子系统(例如在某些嵌入式系统中),那么 pci_enable_device 的实际实现(位于 drivers/pci/pci.c)不会被编译进内核。此时,内联函数的定义(返回 EIO)会作为一个“空实现”或“占位符”,确保代码在未启用 PCI 时仍然可以编译通过。
  • 模块化设计:内核的头文件需要为所有可能的配置提供默认行为,即使某些功能未启用。

2. 实际实现与内联函数的优先级

在 drivers/pci/pci.c 中,pci_enable_device 是一个实际实现的函数,并且通过 EXPORT_SYMBOL 导出,供其他模块使用。当 PCI 子系统启用时,链接器会优先使用 pci.c 中的实际实现,而不是头文件中的内联函数。

3. 头文件中的内联函数的作用

头文件中的内联函数定义(返回 -EIO)主要起到以下作用:
  • 提供默认行为:当 PCI 子系统未启用时,调用 pci_enable_device 会返回 EIO,表示设备无法启用。
  • 编译时检查:确保代码在未启用 PCI 时仍然可以编译通过,避免链接错误。

具体实现机制

1. 头文件中的定义

在 include/linux/pci.h 中,pci_enable_device 的定义如下:
static inline int pci_enable_device(struct pci_dev *dev) { return -EIO; }
这是一个内联函数,直接返回 -EIO。它的作用是:
  • 当 PCI 子系统未启用时,提供一个默认实现。
  • 避免未启用 PCI 时调用未定义的函数。

2. 实际实现

在 drivers/pci/pci.c 中,pci_enable_device 的实际实现如下:
int pci_enable_device(struct pci_dev *dev) { return pci_enable_device_flags(dev, IORESOURCE_MEM | IORESOURCE_IO); } EXPORT_SYMBOL(pci_enable_device);
这个实现会真正启用 PCI 设备的 I/O 和内存资源,并唤醒设备。

3. 条件编译的触发

在 Linux 内核中,是否启用 PCI 子系统是通过配置选项(CONFIG_PCI)控制的。如果 CONFIG_PCI 未定义或未启用,则 drivers/pci/pci.c 不会被编译,此时头文件中的内联函数会生效。

示例场景

场景 1:PCI 子系统启用

  • CONFIG_PCI 被定义。
  • drivers/pci/pci.c 被编译,pci_enable_device 的实际实现可用。
  • 调用 pci_enable_device 时,实际使用的是 pci.c 中的实现。

场景 2:PCI 子系统未启用

  • CONFIG_PCI 未定义。
  • drivers/pci/pci.c 未被编译,pci_enable_device 的实际实现不可用。
  • 调用 pci_enable_device 时,使用的是头文件中的内联函数,返回 EIO

总结

  • 头文件中的内联函数:提供默认行为,确保代码在 PCI 子系统未启用时仍然可以编译通过。
  • 实际实现:当 PCI 子系统启用时,实际使用的是 drivers/pci/pci.c 中的实现。
  • 条件编译:通过 CONFIG_PCI 控制是否启用 PCI 子系统,从而决定使用哪种实现。

那 Linux ,是如何通过连接器实现的这个功能呢?

在 Linux 内核中,通过连接器(链接器,Linker)实现函数的多重定义和优先级选择,主要依赖于以下几个机制:
  1. 符号的强弱性(Symbol Strength)
      • 强符号(Strong Symbol):在目标文件中定义的函数或变量,如果有多个强符号定义,链接时会报错。
      • 弱符号(Weak Symbol):在目标文件中定义的函数或变量,如果有多个弱符号定义,链接时会选择其中一个。
  1. 内联函数的处理
      • 内联函数在编译时会被直接展开,不会生成独立的符号。
      • 如果内联函数没有被展开(例如,编译器决定不内联),则会生成一个弱符号。
  1. 链接器的符号解析规则
      • 如果存在强符号和弱符号,链接器会选择强符号。
      • 如果只有弱符号,链接器会选择其中一个。

具体实现步骤

  1. 编译阶段
      • 当编译一个源文件时,编译器会生成目标文件(.o 文件)。
      • 如果源文件中包含内联函数的定义,编译器会尝试将其内联展开。如果无法内联,则会生成一个弱符号。
  1. 链接阶段
      • 链接器将所有目标文件和库文件合并,生成最终的可执行文件或内核模块。
      • 在链接过程中,链接器会解析所有符号的引用,并根据符号的强弱性选择合适的定义。

示例分析

以 pci_enable_device 为例:
  1. 头文件中的内联函数
    1. static inline int pci_enable_device(struct pci_dev *dev) { return -EIO; }
      • 这个内联函数在编译时会被展开,不会生成独立的符号。
      • 如果编译器决定不内联,则会生成一个弱符号。
  1. 实际实现
    1. int pci_enable_device(struct pci_dev *dev) { return pci_enable_device_flags(dev, IORESOURCE_MEM | IORESOURCE_IO); } EXPORT_SYMBOL(pci_enable_device);
      • 这个函数在 drivers/pci/pci.c 中定义,是一个强符号。
  1. 链接过程
      • 如果 CONFIG_PCI 被定义,drivers/pci/pci.c 会被编译,生成的目标文件中包含 pci_enable_device 的强符号。
      • 如果 CONFIG_PCI 未定义,drivers/pci/pci.c 不会被编译,目标文件中不包含 pci_enable_device 的强符号。
      • 在链接时,如果存在强符号,链接器会选择强符号;如果只有弱符号,链接器会选择弱符号。

具体链接器行为

  1. 强符号存在
      • 如果 drivers/pci/pci.o 被链接,其中包含 pci_enable_device 的强符号。
      • 链接器会选择这个强符号,忽略头文件中的弱符号。
  1. 强符号不存在
      • 如果 drivers/pci/pci.o 未被链接,不包含 pci_enable_device 的强符号。
      • 链接器会选择头文件中的弱符号(如果存在)。

总结

通过链接器的符号解析规则,Linux 内核实现了函数的多重定义和优先级选择:
  • 强符号优先:如果存在强符号,链接器会选择强符号。
  • 弱符号备用:如果只有弱符号,链接器会选择弱符号。
  • 内联函数处理:内联函数在编译时被展开,不会生成独立的符号;如果无法内联,则生成弱符号。
这种机制确保了在不同配置下(例如,是否启用 PCI 子系统),内核能够正确地选择函数的实现,保证代码的灵活性和兼容性。
 
 
欢迎加入喵星计算机技术研究院,原创技术文章第一时间推送。
notion image
 
如何在Debian 11上手动编译安装AMD XGBE 10GB网卡驱动SET_NETDEV_DEV 宏详解