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

记一次热补丁制作失败的问题分析过程

2023-11-28 03:16:09
87
0

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 函数。
0条评论
0 / 1000
王****波
6文章数
0粉丝数
王****波
6 文章 | 0 粉丝
原创

记一次热补丁制作失败的问题分析过程

2023-11-28 03:16:09
87
0

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 函数。
文章来自个人专栏
知识总结
6 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
0
0