【golang踩内存填坑实录】

问题出现

发现项目中某个全局变量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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值