nob 发表于 2024-8-1 09:55

记一次WSL1中Segmentation fault问题的调试

概要:在实机和虚拟机中都能正常运行的Linux程序放到WSL1中却秒报Segmentation fault退出。通过尝试学习Linux下的动态调试工具并配合IDA静态分析,找到了vsyscall这一问题根源(WSL1的一个小坑)。之后通过替换为syscall解决了问题,使程序在WSL1中成功运行。

1. 背景介绍:主机系统Win10,时常运行第三方虚拟机。因为微软自家的虚拟机平台会妨碍第三方虚拟机诸如AVX指令集等的特性传递,所以关闭了相应的功能,从而无法使用WSL2,但WSL1没有问题,而且轻便高效,与“正经”的Linux虚拟机可以相安无事、相得益彰。
      然而,Linux下的某个单纯的CLI计算程序,按说应该很适合放在WSL1中运行,却一运行就报Segmentation fault。虽然放在虚拟机里运行也没有很大影响,但是作为一个折腾爱好者,还是很想搞明白这个问题,并尝试解决一下。


2. 问题定位:本人还是逆向新手,懂一点汇编,用WinDbg和IDA复现过一些简单的逆向操作,但是对理论了解很少,而且不太会自动化。这次的程序倒没有脱壳之类的问题,而首先要解决的是找到Linux下的合适工具。因为是这几天现找现学的,所以用得比较粗浅笨拙,希望各位不要见笑。
      第一个工具是strace,可以跟踪程序的系统调用和信号传递,通过加上-i参数,还能显示出系统调用时指令指针的位置。从strace的输出结果,我看到程序有一个正常的开始,经过若干正常的系统调用后,最终以如下方式结束。

......
read(4, "", 4096)    = 0
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0xffffffffff600400} ---
[????????????????] +++ killed by SIGSEGV (core dumped) +++
Segmentation fault (core dumped)

      从中可以看到Segmentation fault的更进一步信息:代码是SEGV_MAPERR,大意是内存映射错误;对应地址是0xffffffffff600400,按我理解是一个“明显溢出”的地址。马后炮的说,如果我当时就去百度一下这个地址,应该能更早定位这个问题。
      为了搞清这个“指针溢出”是怎么发生的,我下一步想要进行动态调试,这就轮到第二个工具——gdb了。不过首先要解决gdb找不到调试符号的问题。

      首先使用info files命令找到程序的入口地址Entry point: 0x400400,然后用break *0x400400下断点,再执行run就会停在程序入口,之后就能执行各种单步运行、下断点、反汇编的指令了。之前strace得到的指令地址也可以用来下断点,有助于尽快接近结束位置。一顿操作之后,找到了这段代码:

0x0000000001c5a4e0:sub   rsp, 8
0x0000000001c5a4e4:mov   rax, 0xffffffffff600400
0x0000000001c5a4eb:call   rax
0x0000000001c5a4ed:add   rsp, 8
0x0000000001c5a4f1:retn


3. 成因解释:和我预想不同,那个“明显溢出”的地址并非由程序错误导致,而是写死在代码里的。而且在这段代码下面还有一段类似的访问0xffffffffff600000的代码。这个发现促使我重新去审视这两个地址的意义,经过搜索,参考这篇文章才得知这是linux内核在ffffffffff600000-ffffffffff601000预留的vsyscall位置,封装了3个系统调用函数gettimeofday, time和getcpu,借用一张图:

使用cat /proc/self/maps命令,可以看到区别:
在可以运行的系统中:

......
7f60750ee000-7f60750ef000 rw-p 00000000 00:00 0
7ffec1210000-7ffec1231000 rw-p 00000000 00:00 0                        
7ffec1238000-7ffec123b000 r--p 00000000 00:00 0                        
7ffec123b000-7ffec123d000 r-xp 00000000 00:00 0                        
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  

在WSL1中:

......
7f72aaaa0000-7f72aaaa1000 rw-p 0000a000 00:00 273107             /usr/bin/cat
7fffe3d60000-7fffe3d81000 rw-p 00000000 00:00 0                  
7fffea617000-7fffeae17000 rw-p 00000000 00:00 0                  
7fffeb056000-7fffeb057000 r-xp 00000000 00:00 0                  

可知WSL1没有映射vsyscall这段,这就是导致Segmentation fault的原因。

4. 问题解决:了解成因后也找到了相关的issue,只是现在已经被关闭了。关闭的理由是WSL2已经可以设置内核选项 kernelCommandLine = vsyscall=emulate 来修复这个问题,而WSL1就没人管了。好在定位了问题以后,解决方案也比较明确了:只要把vsyscall的调用都换成syscall就行了。rax中的地址换成对应的系统调用序号(可以在 /usr/include/x86_64-linux-gnu/asm/unistd_64.h 中找到),call换成syscall (0F05),其它寄存器等都是兼容的不用修改。



这一修改不改变指令长度,完成gettimeofday和time对应的两处修改后程序在WSL1中成功运行。

PS:现今的多数内核也需要添加启动参数vsyscall=emulate才会映射vsyscall,否则一样会Segmentation fault。隐约记得早先装虚拟机时为了补救Meltdown补丁导致性能下降的问题加了这个参数,没想到隔了这么久又遇到了。

nob 发表于 2024-8-10 07:58

本帖最后由 nob 于 2024-8-10 08:04 编辑

这几天又用gdb调试了另外一个SIGILL的问题,错误代码是ILL_ILLOPN(非法操作数)。
发现其实没必要下断点、单步调试。
可以直接在gdb中run到程序收到异常终止信号,而此时程序并不会立即终止,仍可以在gdb中进行各种调试操作。
例如用backtrace命令(简写为bt)查看栈,就能立即定位到函数调用的来源,再结合反汇编等操作就能快速找到问题的来源了。

主楼中调用vsyscall的问题大概只有几年前的程序会遇到,例如主楼中这个程序应该是2016年的。
可以理解内核参数vsyscall=emulate就是为了兼容这些旧版程序。

PS: 这次遇到的SIGILL的问题是一个近期的开源程序,调试找到的问题原因是老电脑CPU不支持BMI2指令集……
发布的二进制程序用不了,好在可以自己编译解决。
不得不意识到时间跨度带来的各种兼容性问题。

rmb788520 发表于 2024-11-9 08:09

感谢楼主。受益良多

Mayrain555 发表于 2024-11-9 08:36

感叹系统的事儿真是如同黑箱,报错的原因可能只是因为很小的一个问题。受益良多。
页: [1]
查看完整版本: 记一次WSL1中Segmentation fault问题的调试