1 问题现象:
鲲鹏LPI中断性能优化补丁,涉及如下三个patch(引用自《鲲鹏LPI中断性能优化补丁分析.docx》)
在准备将以上修改制作成热补丁时,制作报错,报错信息如下:
报错信息表明,由于这些patch中的修改内容,导致了.init.text段发生变化,导致.init.text变化的修改不能制作成内核热补丁(初始化函数只执行一次,补丁函数执行不到)。
2 问题分析过程:
根据错误提示信息显示,不能制作成热补丁的原因是.init.text段发生了变化,所以首先检查补丁的修改情况,看是否有修改到.init.text的代码。
背景知识:如何判断函数是放在.init.text段的?
在内核的代码中,通过__init宏告知编译器,将变量或函数放到.init.text代码段中
相关的宏定义在include/linux/init.h中
#define __init __section(.init.text) __cold __latent_entropy __noinitretpoline
#define __initdata __section(.init.data)
#define __initconst __section(.init.rodata)
#define __exitdata __section(.exit.data)
#define __exit_call __used __section(.exitcall.exit)
如下示例表示将its_lpi_init()函数放到.init.text段
根据背景知识,首先排查这几个patch有没有新增、修改__init的函数,排查情况如下。
2.1 分析各个补丁代码修改情况
1、2f13ff1d1d ("irqchip/gic-v3-its: Track LPI distribution on a per CPU basis")
详细的修改信息如下:
从commit的详细修改可知:
- 修改its_set_affinity()、its_irq_domain_activate()、its_irq_domain_deactivate(),这三个函数都不在.init.text段。
- 新增了its_read_lpi_count()、its_inc_lpi_count()、its_dec_lpi_count(),这三个新增的函数也不在.init.text段。
2、c5d6082d3 (“irqchip/gic-v3-its: Balance initial LPI affinity across CPUs”)
详细修改信息如下:
从commit的详细修改可知:
- 修改its_set_affinity()、its_irq_domain_activate()两个函数,这两个函数不在.init.text段。
- 新增了cpumask_pick_least_loaded()、its_select_cpu()两个函数,新增的函数都不是在.init.text段。
小结:
汇总来看,这两个commit:
- 修改了3个函数:its_set_affinity()、its_irq_domain_activate()、its_irq_domain_deactivate()
- 新增了5个函数:its_read_lpi_count()、its_inc_lpi_count()、its_dec_lpi_count()、cpumask_pick_least_loaded()、its_select_cpu()
这些新增修改的函数都不在.init.text段,那是什么导致有变化的呢?会不会是有init.text段的函数调用了这些接口导致的?
2.2 分析各个新增修改函数的调用关系
its_set_affinity():
its_set_affinity()是通过函数指针注册的,函数内部逻辑的变化不会导致调用者有任何变化。
its_irq_domain_activate():
its_irq_domain_activate()也是通过函数指针注册的,函数内部逻辑的变化不会导致调用者有任何变化。
its_irq_domain_deactivate():
与its_irq_domain_activate()一样,也不会有任何影响
its_read_lpi_count():
最终被its_irq_domain_activate()或its_set_affinity()调用,这两个函数没问题,its_read_lpi_count()也没问题。
its_inc_lpi_count():
与its_read_lpi_count()一样也没问题
its_dec_lpi_count():
同理,也没问题
cpumask_pick_least_loaded():
也没问题
its_select_cpu():
也没问题
小结:
汇总来看,所有的函数调用关系都排查完了,都没有问题,那是哪导致变化了呢?
问题分析到这,进入了死胡同,既然正向找不出来哪的变化导致的,也没有其他什么好办法,那就看看.init.text究竟有什么变化吧
2.3 分析.init.text的数据差异
使用到的命令:
readelf -x .init.text orig/drivers/irqchip/irq-gic-v3-its.o //获取打patch前的init.text段的数据
readelf -x .init.text patched/drivers/irqchip/irq-gic-v3-its.o //获取打patch后的init.text段的数据
用比较工具查看差异,发现数据确实有变化,红色部分所示:
这些全是二进制数据,看不出是什么地方有变化了,准备用objdump看看能不能找到具体差异的地方。
使用到的命令:
objdump -dsl -j .init.text orig/drivers/irqchip/irq-gic-v3-its.o
objdump -dsl -j .init.text patched/drivers/irqchip/irq-gic-v3-its.o
发现变化的地方在its_lpi_init()函数中,如下所示:
左侧是打patch前的代码,右侧是打patch后的代码
由上图可知,是一个立即数参数发生了变化,由0x118变成了0x150, 两者相差0x38。
its_lpi_init(): /usr/src/linux-4.19.90-2102.2.0.0062.ctl2.aarch64/drivers/irqchip/irq-gic-v3-its.c:1741 bc4: 90000000 adrp x0, 0 <gic_acpi_match_srat_its> bc8: 91000000 add x0, x0, #0x0 bcc: 90000001 adrp x1, 0 <gic_acpi_match_srat_its> bd0: 2a1503e2 mov w2, w21 bd4: 91000021 add x1, x1, #0x0 bd8: 91054000 add x0, x0, #0x150 bdc: 94000000 bl 0 <__dynamic_pr_debug> |
查看its_lpi_init()函数,确实是在.init.text段中,但是patch没有修改这个函数,为什么它会有变化呢?
根据反汇编显示,差异的行在1741那一行,上图红框的部分,至此经历过的人可能已经猜出了问题所在。
对于没有经历过的,问题已经陷入了绝境,1741那一行,只是一行打印而已,而且打印的参数也是一个局部变量,并且本次的patch也没有修改这个函数,所以its_lpi_init()是无论如何都没有理由会变的。
没有其他办法,那就只有使用终极大法了,既然变化是由于patch的修改带来的,那就把patch的修改一个一个的屏蔽掉,看究竟是哪个函数的修改导致的。
2.4 逐一屏蔽修改点,找出问题引入的函数
将patch的修改逐一屏蔽,当屏蔽掉以下新增函数(its_select_cpu())后,.init.text没有变化了,看了一下这个函数的实现,它不在init.text段,是怎么导致有变化的呢?
有时候发现问题所在的点,可能就在那不经意的一眼,这次不经意间发现这个函数中也调用了pr_debug,前面反汇编指示有变化的地方也在pr_debug那。
所以果断的把所有修改恢复,只屏蔽到新增的pr_debug这一行,经验证问题解决。
问题引入的点已经找到了,接下来就是分析为什么pr_debug会带来这样的影响。
2.5 问题原因探究
问题既然是pr_debug引入的,那就看看pr_debug的具体实现究竟是什么样的,经过代码逐级跟踪,发现,pr_debug会在__verbose段放入一个struct _ddebug的数据结构,而这个数据结构的地址会作为参数传递给_dynamic_pr_debug(), 那个变化的立即数,就与这个数据结构在__verbose段中的相对位置有关。
使用crash工具查看struct _ddebug的大小,正好是0x38,如下:
与前面发现.init.text的立即数由0x118变成了x0150,相差0x38相吻合,至此问题原因明确了。
3 问题原因说明
pr_debug()会在__verbose段放入一个struct _ddebug的数据结构,这个数据结构的地址会作为参数传递给_dynamic_pr_debug(),本次修改在新增函数its_select_cpu()(在引起变化的函数its_lpi_init()前)中调用了pr_debug(),导致its_lpi_init()函数中的struct _ddebug数据结构的相对位置发生了变化,从而导致调用_dynamic_pr_debug()时传递的参数发生变化,最终表面在.init.text段发生了变化。
4 热补丁约束限制
不支持的函数修改行为
- 不支持修改函数参数或返回值类型或个数。
- 不支持删除函数。
- 不支持修改数据结构成员(热补丁原理是做函数替换)。
不支持的文件修改行为
- 不支持修改汇编文件。
- 不支持修改头文件。
- 不支持修改非C语言编写的文件。
不支持的变量修改行为
- 不允许删除全局变量或函数内部静态局部变量。
- 不支持修改全局变量或静态局部变量初始值。
- 不支持新增同名静态局部变量。
- 不支持修改多个同名静态局部变量的引用顺序。
不支持的函数类型
- 不支持对初始化函数打补丁(初始化函数只执行一次,补丁函数执行不到)。
- 不支持对死循环、不退出函数打补丁(旧函数不退出调用栈,没有机会调用新函数)。
- 不允许对NMI中断的处理函数打补丁( stop machine无法stop住NMI中断处理流程,补丁无法保证对该类函数打补丁的一致性和安全性)。
- 不支持对修改前后内敛情况发生变化的函数打补丁。
- 不支持编译器生成的函数名称在修改前后发生变化的函数打补丁,例如修改前编译器生成的函数名为“ do_oops_enter_exit.part.0”,修改后编译器生成的函数名为“ do_oops_enter_exit”。
- 不支持对arm64架构下长度小于4条指令的超小函数打补丁。(这种情况可以通过对外围函数打补丁来解决)
- 不支持对启用ftrace、 kprobe等修改指令机制的函数打补丁。(打热补丁会使被修改函数的ftrace/kprobe机制失效)
- 不支持对以下idle进程相关函数打补丁:
- call_cpuidle
- cpuidle_idle_call
- do_idle
- 不支持对包含以下弱符号的函数打补丁。
- kallsyms_addresses
- kallsyms_num_syms
- kallsyms_names
- kallsyms_markers
- kallsyms_token_table
- kallsyms_token_index
- kallsyms_offsets
- 不支持对包含下列范围之外的其他代码段的函数打补丁:
- .text
- __bug_table
- .fixup
- __ex_table
- __jump_table
- .smp_locks
- .parainstructions
- .altinstructions
- 不支持在.altinstructions 段中修改 ALTINSTR_ENTRY_CB 类型 alt_instr 的函数打补丁。例如 arm64 架构下 kvm 模块中的 kern_hyp_va 函数。