从一次栈溢出问题讨论thread_local变量与线程栈

我的开发环境,linux系统、x86_64架构

一.栈溢出问题记录

1.背景

大家都知道栈的大小是有上限的,在linux下可以通过命令ulimit -s查看栈的size上限,也可以使用ulimit -a。我的机器默认是8M:

stack_size.png

并且,我们也可以通过ulimit -s命令来设置这个上限。大多数情况下,这个8M的空间已经够用了。但是偶尔也会遇到栈空间不足的情况。栈空间不足我们遇到更多的是下面两种情况:

1.1 函数调用栈帧太深

这种一般见于递归调用栈帧太深或者发生了死循环调用,直到把栈撑爆。我所在的项目,之前遇到过一个函数的const版本实现有问题,导致函数的非const版本和const版本死循环调用,最后栈撑爆,线上服务core掉。

1.2 函数内的非静态局部变量占用空间太大

最简单的一种是直接在函数内放一个数组char buff[100*1024*1024]; ,这种也会轻松把栈撑满,导致程序无法继续运行。

2.问题过程

上面这两种情况,对于一个有经验的程序员,只需要gdb一挂,分分钟就看出问题,甚至仔细看一下代码,就能分析出问题所在。然而我们这次遇到了一个很罕见的问题:现象是,服务器在启动时会core在一个线程的入口函数处(即pthread_create函数的第三个参数)。汇编展开以后发现最后的指令就是一个mov指令,并且操作数都是看起来很正常的。
这个时候组里几个大佬一起分析了一下近期的提交,发现在把中间某个版本的修改回退以后,服务就可以正常起来了。看了这个版本提交以后,发现中间在UserDB上加了一个数组ar,使UserDB大小增长了不少,由于UserDB是放在共享内存的,所以我们我们怀疑是不是资源占用太多,或者中间共享内存上数据的初始化有问题。这个时候另外一个大佬发现,把数组ar的长度减小到1,服务可以正常启动,这个时候我们更坚信的是我们对共享内存上的数据处理有问题。然而却走错了方向。在经历了一番折腾以后,发现并没有任何收获。最后不得不回到起点,从core文件开始分析。这次我们仔细的disassemble一下(为了方便描述,图片处理过):


dis2.png

我们发现在最初的一波寄存器压栈以后,在第9行进行了栈顶指针sp的一个移动操作,即分配了栈上的内存,为下面的局部变量使用,但是在紧接着第11行core掉了。此处只是简单的向刚分配的栈上内存的一个赋值操作。我们紧接着看了两个操作数的内容:

r.png

看着貌似内容也是合法值。那么现在能想到的只有一个解释了,sp已经指向了一个stack外部的地址了——栈溢出了。下面就是验证问题了。我们ulimit -s 20480把栈的上限从8M改成了20M,然后重新起服务,果不其然,服务正常起来了。问题已经确认,为什么栈会溢出。首先,我们看过core文件的函数调用栈,调用层次并不深,可以排除由于函数栈太深的原因。现在就是要确认栈上存的到底是什么数据,会导致栈被塞满。我找了栈帧上所有的函数的局部变量,占用总空间也不会超过1M。又陷入了僵局。
这个时候团队的力量又发挥了作用,组里另外一个大佬指出,自己为了提升性能,减少内存分配次数,做了一个内存cache的模板类:负责管理经常被使用、对象个数不会太多、线程能独占的局部变量。然后这个cache类会用thread_local的形式来存放被管理的对象。static thread_local修饰的局部变量大家经常使用,但是很少有人注意过这种变量的存放位置。
大佬把这个管理类修改了一下,利用一个宏屏蔽了其中的static thread_local的存储方式。重新编译,栈重新改回8M上限,服务可以正常起来了。可以得出一个初步的结论,这种static thread_local的变量挤占了栈的空间。问题虽然解决,但是why?我们下面具体分析。

二.thread_local变量分析

现在用一段简单的代码来分析一下thread_local变量

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>

void *func(void *arg)
{
    static thread_local int64_t x = 99;
    thread_local int64_t y = 999;
    int64_t l = 9999;
    printf("addl = %p, addr x = %p, addr y = %p, diff = %ld \n", &l, &x, &y, (int64_t)&y - (int64_t)&x);
    while (true) {
    }
}

int main()
{
    int64_t m_l = 12345;
    printf("addr m_l = %p\n", &m_l);
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_t tid4;
    pthread_t tid5;
    pthread_create(&tid1, NULL, func, NULL);
    pthread_create(&tid2, NULL, func, NULL);
    pthread_create(&tid3, NULL, func, NULL);
    pthread_create(&tid4, NULL, func, NULL);
    pthread_create(&tid5, NULL, func, NULL);
    sleep(1000);
    return 0;
}

输出是这个样子的:

addr m_l = 0x7fff02a057e8
addl = 0x7fd255762f08, addr x = 0x7fd2557636f8, addr y = 0x7fd2557636f0, diff = -8 
addl = 0x7fd254f61f08, addr x = 0x7fd254f626f8, addr y = 0x7fd254f626f0, diff = -8 
addl = 0x7fd254760f08, addr x = 0x7fd2547616f8, addr y = 0x7fd2547616f0, diff = -8 
addl = 0x7fd253f5ff08, addr x = 0x7fd253f606f8, addr y = 0x7fd253f606f0, diff = -8 
addl = 0x7fd25375ef08, addr x = 0x7fd25375f6f8, addr y = 0x7fd25375f6f0, diff = -8

然后我们再看一下内存布局(篇幅问题,只复制了其中的一部分):

7fd252f5f000-7fd252f60000 ---p 00000000 00:00 0 
7fd252f60000-7fd253760000 rw-p 00000000 00:00 0                          [stack:10354]
7fd253760000-7fd253761000 ---p 00000000 00:00 0 
7fd253761000-7fd253f61000 rw-p 00000000 00:00 0                          [stack:10353]
7fd253f61000-7fd253f62000 ---p 00000000 00:00 0 
7fd253f62000-7fd254762000 rw-p 00000000 00:00 0                          [stack:10352]
7fd254762000-7fd254763000 ---p 00000000 00:00 0 
7fd254763000-7fd254f63000 rw-p 00000000 00:00 0                          [stack:10351]
7fd254f63000-7fd254f64000 ---p 00000000 00:00 0 
7fd254f64000-7fd255764000 rw-p 00000000 00:00 0                          [stack:10350]

我们现在看thread_local变量x、y的地址,是在stack的地址空间内的。验证了我们之前的结论:在linux下,这种thread_local的变量会挤占栈的空间。(并且在C++11标准下,thread_local的作用等同于static thread_local
另外,上面这位大佬统计了项目中符号表中TLS变量的综合,已经超过5M,已经超过8M的一半了,亟需整理。

TODO:可以利用pthread_attr_getstack/pthread_attr_setstack等一系列线程栈的操作来更详细的分析上述问题

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,711评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,079评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,194评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,089评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,197评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,306评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,338评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,119评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,541评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,846评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,014评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,694评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,322评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,026评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,257评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,863评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,895评论 2 351

推荐阅读更多精彩内容