近段时间,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。
两条执行路径
先跟踪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的自动过滤机制抑制掉第二个输出,就只剩一个输出了。
(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)
*************************************************
正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生
扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物
也欢迎关注格友公众号