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 个文件中

通过观察这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)实现函数的多重定义和优先级选择,主要依赖于以下几个机制:
- 符号的强弱性(Symbol Strength):
- 强符号(Strong Symbol):在目标文件中定义的函数或变量,如果有多个强符号定义,链接时会报错。
- 弱符号(Weak Symbol):在目标文件中定义的函数或变量,如果有多个弱符号定义,链接时会选择其中一个。
- 内联函数的处理:
- 内联函数在编译时会被直接展开,不会生成独立的符号。
- 如果内联函数没有被展开(例如,编译器决定不内联),则会生成一个弱符号。
- 链接器的符号解析规则:
- 如果存在强符号和弱符号,链接器会选择强符号。
- 如果只有弱符号,链接器会选择其中一个。
具体实现步骤
- 编译阶段:
- 当编译一个源文件时,编译器会生成目标文件(
.o
文件)。 - 如果源文件中包含内联函数的定义,编译器会尝试将其内联展开。如果无法内联,则会生成一个弱符号。
- 链接阶段:
- 链接器将所有目标文件和库文件合并,生成最终的可执行文件或内核模块。
- 在链接过程中,链接器会解析所有符号的引用,并根据符号的强弱性选择合适的定义。
示例分析
以
pci_enable_device
为例:- 头文件中的内联函数:
- 这个内联函数在编译时会被展开,不会生成独立的符号。
- 如果编译器决定不内联,则会生成一个弱符号。
static inline int pci_enable_device(struct pci_dev *dev) { return -EIO; }
- 实际实现:
- 这个函数在
drivers/pci/pci.c
中定义,是一个强符号。
int pci_enable_device(struct pci_dev *dev) { return pci_enable_device_flags(dev, IORESOURCE_MEM | IORESOURCE_IO); } EXPORT_SYMBOL(pci_enable_device);
- 链接过程:
- 如果
CONFIG_PCI
被定义,drivers/pci/pci.c
会被编译,生成的目标文件中包含pci_enable_device
的强符号。 - 如果
CONFIG_PCI
未定义,drivers/pci/pci.c
不会被编译,目标文件中不包含pci_enable_device
的强符号。 - 在链接时,如果存在强符号,链接器会选择强符号;如果只有弱符号,链接器会选择弱符号。
具体链接器行为
- 强符号存在:
- 如果
drivers/pci/pci.o
被链接,其中包含pci_enable_device
的强符号。 - 链接器会选择这个强符号,忽略头文件中的弱符号。
- 强符号不存在:
- 如果
drivers/pci/pci.o
未被链接,不包含pci_enable_device
的强符号。 - 链接器会选择头文件中的弱符号(如果存在)。
总结
通过链接器的符号解析规则,Linux 内核实现了函数的多重定义和优先级选择:
- 强符号优先:如果存在强符号,链接器会选择强符号。
- 弱符号备用:如果只有弱符号,链接器会选择弱符号。
- 内联函数处理:内联函数在编译时被展开,不会生成独立的符号;如果无法内联,则生成弱符号。
这种机制确保了在不同配置下(例如,是否启用 PCI 子系统),内核能够正确地选择函数的实现,保证代码的灵活性和兼容性。
欢迎加入“喵星计算机技术研究院”,原创技术文章第一时间推送。

- 作者:tangcuyu
- 链接:https://expoli.tech/articles/2025/02/28/Why-are-there-multiple-implementations-of-the-same-function-in-Linux-and-What-is-the-strength-of-symbols
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章