问题出现
发现项目中某个全局变量initialized在执行过程中被篡改了。
var initialized int32
分析
分析程序内代码没有修改initialized的值,怀疑是cgo导致的内存被篡改。
首先,因为dlv无法调试cgo代码,且dlv没有watch内存的功能,所以用gdb调试。
watch
如下编译可执行文件,避免无法调试的情况
go build -gcflags "-N -l" -ldflags=-compressdwarf=false
直接在gdb中,直接watch initialized,虽然对应内存的value变动了,但是不会触发断点。
(gdb) watch 'xxxxx.initialized'
Hardware watchpoint 1: 'xxxxx.initialized'
起初以为是对cgo或者是cgo的动态库无效,经过试验发现实例程序都可以触发断点。
valgrind
$ valgrind --tool=memcheck --leak-check=full ./backEnd
使用valgrind也没能发现内存问题的位置。
bss
所幸initialized是全局变量,在编译时就已经分配好地址。未初始化的全局变量存放在bss段里。
因为多次运行,initialized变量都被篡改,说明不是随机踩内存,应该是越界踩。由此可以推断,bss低地址变量内存越界了。
关于go内存地址空间,可以查看这两篇文章,进程地址空间划分,GO MEMORY MANAGEMENT
插曲
一开始怀疑heap的起始位置错误,导致部分内存占用到bss的空间了。
initialized对应的地址为0x768f564
而bss的地址范围如图,由此可见initialized这个地址并不是bss中最高位置,肯定不会跟heap冲突,不然还会有更多问题。
otool -l backEnd
(gdb) info symbol 0x768f564
github.com/hyperledger/fabric-sdk-go/pkg/core/cryptosuite.ina in section __DATA.__noptrbss
watch 工具定位内存越界代码
怀疑内存越界,已知越界的大小是32位,且每次都是如此,那么肯定是低地址的变量发生了越界。
把地址向上减少些,发现上一个全局变量是gosuri/uilive.sz
(gdb) p 'xxxxxx/cryptosuite.initialized'
$1 = 0
(gdb) p &'xxxxxx/cryptosuite.initialized'
$2 = (int32 *) 0x77d05d4 <xxxxxx/cryptosuite.initialized>
(gdb) info symbol 0x77d05d3
xxxxx/vendor/github.com/gosuri/uilive.sz + 3 in section __DATA.__noptrbss
在sz变动的代码前后加上断点可以确认,确实是这里出现的内存越界。
真相
分析内存越界代码,其中/dev/tty是用于获取terminal数据的,在执行syscall时发生了内存越界,发现sz的变量类型windowSize声明错了。
func getTermSize() (int, int) {
if runtime.GOOS == "openbsd" {
out, err = os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return 0, 0
}
} else {
out, err = os.OpenFile("/dev/tty", os.O_WRONLY, 0)
if err != nil {
return 0, 0
}
}
_, _, _ = syscall.Syscall(syscall.SYS_IOCTL,
out.Fd(), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&sz)))
fmt.Println(binary.Size(sz))
return int(sz.cols), int(sz.rows)
}
windowSize
github.com/uilive
中windowSize
type windowSize struct {
rows uint16
cols uint16
}
- kernel中winsize
https://man7.org/linux/man-pages/man4/tty_ioctl.4.html
The struct used by these ioctls is defined as
struct winsize {
unsigned short ws_row;
unsigned short ws_col;
unsigned short ws_xpixel; /* unused /
unsigned short ws_ypixel; / unused */
};
显然,winsize应该是64位,而uilive里将其设为了32位,导致内存越界,影响到后面的变量了。
之前有同事发现,initialized被篡改的情况在vscode的集成terminal和docker容器不会出问题,其实跟/dev/tyy返回的值有关系,这两种情况下获取的像素值ws_xpixel,ws_ypixel
都为0,其实也是越界了,只是业务逻辑中initialized为0时会重新触发初始化,所以问题没有很明显。
总结
go的内存管理总体来说比较省心,一般也很少去分析内存踩踏的情况,但是这次采坑告诉了我,go的内存并不是不会出现问题的,一样要抱着怀疑的态度去思考。
参考
[1]https://www.cnblogs.com/arnoldlu/p/10272466.html
[2]https://man7.org/linux/man-pages/man4/tty_ioctl.4.html
[3]https://stackoverflow.com/questions/18878141/difference-between-structures-ttysize-and-winsize