地址歧义和GCC的不可能逻辑

近段时间,NDB的x命令在显示函数地址时,可能给出两个不同的地址,比如:

x llaolao!proc_lll_read
ffffff80`0181d3ac  llaolao!proc_lll_read (file*, char*, size_t, loff_t*)
ffffff80`0181dcb8  llaolao!proc_lll_read

上述两行信息来自两个信息源,一个是DWARF格式的符号,另一个是ELF符号。

NDB内部有不同的函数分别处理DWARF符号和ELF符号,它们给出的结果不一致了。

今日有些闲暇,我决定亲手抓一抓这个BUG。

18ffd30e27f1839567c2aab503419cbd.jpeg

两条执行路径

先跟踪DWARF的代码,它是根据DW_TAG_subprogram块中的low_pc属性来做为偏移地址的。DW_AT_low_pc的值为0x3ac,加上llaolao模块的基地址ffffff80`0181d000,便得到结果ffffff80`0181d3ac。

<1><c4a4>: Abbrev Number: 85 (DW_TAG_subprogram)
    <c4a5>   DW_AT_name        : (indirect string, offset: 0x3849a): proc_lll_read
    <c4a9>   DW_AT_decl_file   : 1
    <c4a9>   DW_AT_decl_line   : 110
    <c4aa>   DW_AT_decl_column : 16
    <c4ab>   DW_AT_prototyped  : 1
    <c4ab>   DW_AT_type        : <0x296>
    <c4af>   DW_AT_low_pc      : 0x3ac
    <c4b7>   DW_AT_high_pc     : 0x110

再跟踪ELF符号的解析代码,它查找ELF符号,得到的偏移地址居然也是0x3ac。

使用readelf -s观察,地址信息也确实是3ac:

geduer@gdk8:~/gelabs/llaolao$ readelf -s llaolao.ko | grep lll_read
   766: 00000000000003ac   272 FUNC    LOCAL  DEFAULT  357 proc_lll_read

但是ELF解析代码没有简单的加模块基地址,而是根据ELF符号所在的节,查找节的基地址,然后加上节的基地址0xffffff800181d90c,得到ffffff80`0181dcb8。

如此看来,两种解析方法得到的函数地址偏移是一致的,只是因为加上了不同的基地址,才得出了不同的结果。

“不可能的”段名

跟踪elf符号的输出过程,发现x命令查找的函数是在一个名叫.text.unlikely的段里。

-    sh  {m_name=0x0000004a m_type=0x00000001 m_flags=0x0000000000000006 ...}  tagElf_SectionHeader &
m_name  0x0000004a  unsigned int
m_type  0x00000001  unsigned int
m_flags  0x0000000000000006  unsigned __int64
m_addr  0xffffff800181d90c  unsigned __int64
m_offset  0x0000000000001a0c  unsigned __int64
m_size  0x0000000000000c3c  unsigned __int64
m_link  0x00000000  unsigned int
m_info  0x00000000  unsigned int
m_addralign  0x0000000000000004  unsigned __int64
m_entsize  0x00000000  long
+    m_szName  0x000001d616aac8da ".text.unlikely"  unsigned char *
m_shndx  0x00000165  int

使用cat命令在目标机上观察这个段的属性,它确实存在,基地址也确实是0xffffff800181d90c。

geduer@gdk8:/sys/module/llaolao/sections$ sudo cat .text.unlikely
0xffffff800181d90c

如此看来,ELF路径的代码是对的,DWARF路径的代码处理的不够细致,有BUG。

为何在“不可能的”节?

那么.text.unlikely节是什么意思呢?

回答这个问题之前,不得不说Linux的内核驱动模块(KO时)包含非常多的节,数量多的有点惊人。即便是刘姥姥这么一个小驱动,产生的节数居然有1112个,真是多的到了需要整改的地步。

geduer@gdk8:~/gelabs/llaolao$ readelf -S llaolao.ko | tail
       00000000000073b0  0000000000000018          1111   1173     8
  [1111] .strtab           STRTAB           0000000000000000  00156c80
       0000000000004ac6  0000000000000000           0     0     1
  [1112] .shstrtab         STRTAB           0000000000000000  001cc890
       000000000000023a  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), p (processor specific)

其中确实就包含.text.unlikely节。

geduer@gdk8:~/gelabs/llaolao$ readelf -S llaolao.ko | grep unlikely
  [357] .text.unlikely    PROGBITS         0000000000000000  00001a0c

加载到内存中后,有些节被合并掉了,有些节被忽略了。对于刘姥姥驱动,还有如下这么多个节:

geduer@gdk8:/sys/module/llaolao/sections$ ls -a
.                      .data                      .init.text          .rodata.str     .text.ftrace_trampoline
..                     .eh_frame                  .note.Linux         .rodata.str1.8  .text.unlikely
.altinstr_replacement  .exit.text                 .note.gnu.build-id  .strtab         __bug_table
.altinstructions       .gnu.linkonce.this_module  .plt                .symtab         __jump_table
.bss                   .init.plt                  .rodata             .text           __param

为什么需要这么多个节呢?

有些是必要的,比如普通的代码放在 .text节里,这是最好理解的。init函数因为初始化之后就不需要了,所以有必要放在单独的.init.text里。 

那么为什么要把proc_lll_read放在.text.unlikely节里呢?

按可能性优化

简单说,这个问题与优化机制有关。为了节约内存资源,gcc的链接程序(ld)有个优化功能:把常用的代码和不常用的代码分开,常用的普通代码放在.text段里,不常用的放在.text.unlikely里。如果开启了-ffunction-sections选项,那么不常用的函数可以每个函数占一个节,节的名字为.text.unlikely.<funcname>。以上信息主要根据Jeff Law在binutils讨论组里的发言:https://binutils.sourceware.narkive.com/aqxec3Kl/fix-for-unlikely-text-section-grouping-in-conjunction-with-gcc-s-ffunction-sections

可能是因为proc_lll_read只是proc文件的回调函数,没有在普通代码里被调用过,所以gcc在编译时认为它是不常用的,链接时被放入了.text.unlikely段里。

网上可以搜索到一些有关的讨论,摘录一例。

Trying to compile mozilla-central with Gcc 8.1.1 on Linux produces this error at multiple places:


{standard input}: Assembler messages:
{standard input}:169100: Error: can't resolve `.text.unlikely' {.text.unlikely section} - `.LVL676' {.text section}
{standard input}:170794: Error: can't resolve `.text.unlikely' {.text.unlikely section} - `.LVL1171' {.text section}
{standard input}:170806: Error: can't resolve `.text.unlikely' {.text.unlikely section} - `.LVL1171' {.text section}
gmake[4]: *** [mozilla/config/rules.mk:1050: Library.o] Error 1

解决

搞清原因后,解决起来并不复杂,因为NDB已经有动态获取基地址的能力,并封装为函数:GetLiveAddress(pElfSym);

此外,还有一个更方便的封装:

void NdwLKM::PublishSymAddr(DW_SYM_TAG* tag, ULONG64& addr)

如此修改后,BUG不见了,两种路径得到相同的地址,NDB的自动过滤机制抑制掉第二个输出,就只剩一个输出了。

(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)

*************************************************

正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生

扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物

f51a9cc17002cffe0380a5d088106e37.png

也欢迎关注格友公众号

4029145012f93c2bb3ea23f1b47fb1d4.jpeg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值