命令行登录mysql报Segmentation fault故障解决

前段时间遇到一个mysql客户端crash的问题,这个mysql客户端是自己源码编译产生的。为了解决这个问题,查阅了很多资料,涉及终端ncurses编程、进程的地址空间(堆和栈)、cmake、gcc编译等,踩了不少坑,好在算是比较好的解决了这个问题。

环境

centos8.4 gcc8.4.1 mysql8.0.21 x86_64

问题描述

对mysql8.0.21源码进行make,由于一开始没安装ncurses库,在链接时遇到错误undefined reference to,后来安装了该库,再次make成功。于是将mysqld启动,再用mysql -u root -p连接,输好密码回车后mysql客户端发生Segmentation fault。

第一次make时有编译警告(第二次make时不会有,因为.o文件在第一次make时已经生成),摘要如下:

/opt/resource/mysql-8.0.21/extra/libedit/libedit-20190324-3.1/src/terminal.c: In function ‘terminal_set’:
/opt/resource/mysql-8.0.21/extra/libedit/libedit-20190324-3.1/src/terminal.c:877:6: warning: implicit declaration of function ‘tgetent’; did you mean ‘getenv’? [-Wimplicit-function-declaration]
  i = tgetent(el->el_terminal.t_cap, term);
      ^~~~~~~
      getenv
/opt/resource/mysql-8.0.21/extra/libedit/libedit-20190324-3.1/src/terminal.c:899:15: warning: implicit declaration of function ‘tgetflag’; did you mean ‘tigetflag’? [-Wimplicit-function-declaration]
   Val(T_am) = tgetflag("am");
               ^~~~~~~~
               tigetflag
/opt/resource/mysql-8.0.21/extra/libedit/libedit-20190324-3.1/src/terminal.c:908:15: warning: implicit declaration of function ‘tgetnum’; did you mean ‘tigetnum’? [-Wimplicit-function-declaration]
   Val(T_co) = tgetnum("co");
               ^~~~~~~
               tigetnum
/opt/resource/mysql-8.0.21/extra/libedit/libedit-20190324-3.1/src/terminal.c:917:19: warning: implicit declaration of function ‘tgetstr’; did you mean ‘tigetstr’? [-Wimplicit-function-declaration]
       char *tmp = tgetstr(strchr(t->name, *t->name), &area);
                   ^~~~~~~
                   tigetstr
/opt/resource/mysql-8.0.21/extra/libedit/libedit-20190324-3.1/src/terminal.c:917:19: warning: initialization of ‘char *’ from ‘int’ makes pointer from integer without a cast [-Wint-conversion]

分析过程

centos8.4默认不能生成core文件,为了能生成core文件,需要对操作系统做如下配置,可以在/etc/rc.d/rc.local文件末尾增加内容,并给rc.local添加执行权限,内容如下:

echo "core-%t.%p" > /proc/sys/kernel/core_pattern

意思是在执行程序的当前目录生成core-为前缀,再带上时间戳和进程号的core文件,比如:core-1637149273.2955,其中1637149273是时间戳,2955是进程号。配置好后需要重启机器才能永久生效;如果不想重启只是想临时生效的话,也可以以root用户执行该语句去修改core_pattern文件内容。

修改core_pattern文件内容生效后,再次使mysql客户端发生Segmentation fault,于是就有了core文件了。

gdb查看core文件的函数堆栈信息如下:

gdb bin/mysql ~/core-1637149273.2955
(gdb) bt
#0  0x00000000004e4eed in terminal_alloc (el=0x286eee0, t=<optimized out>, cap=0x52a9aaa0 <error: Cannot access memory at address 0x52a9aaa0>)
    at /opt/resource/mysql-8.0.21/extra/libedit/libedit-20190324-3.1/src/terminal.c:350
#1  0x00000000004e5da7 in terminal_set (el=el@entry=0x286eee0, term=<optimized out>, term@entry=0x0)
    at /opt/resource/mysql-8.0.21/extra/libedit/libedit-20190324-3.1/src/terminal.c:900
#2  0x00000000004e5ee1 in terminal_init (el=el@entry=0x286eee0) at /opt/resource/mysql-8.0.21/extra/libedit/libedit-20190324-3.1/src/terminal.c:297
#3  0x00000000004ea220 in el_init_internal (prog=0x7ffd52a9c6c9 "./mysql", fin=0x7fcd6f9c09c0 <_IO_2_1_stdin_>, 
    fout=0x7fcd6f9c16e0 <_IO_2_1_stdout_>, ferr=0x7fcd6f9c1600 <_IO_2_1_stderr_>, fdin=0, fdout=fdout@entry=1, fderr=2, flags=128)
    at /opt/resource/mysql-8.0.21/extra/libedit/libedit-20190324-3.1/src/el.c:139
#4  0x00000000004e22d5 in rl_initialize () at /opt/resource/mysql-8.0.21/extra/libedit/libedit-20190324-3.1/src/readline.c:297
#5  0x00000000004e2b55 in read_history (filename=filename@entry=0x286eec0 "/root/.mysql_history")
    at /opt/resource/mysql-8.0.21/extra/libedit/libedit-20190324-3.1/src/readline.c:1359
#6  0x000000000040924a in main (argc=<optimized out>, argv=<optimized out>) at /opt/resource/mysql-8.0.21/client/mysql.cc:1403

来看看terminal.c 350行附近的内容:

看这行的内容应该是内存地址非法访问造成crash的。再来看看terminal.c 900行附近的内容:

从第900行来看,应该是tgetstr函数返回值有问题。tgetstr是ncurse库中的一个函数,为了更好的解决这个问题,有必要来了解一些终端编程的基本概念。

输入:man 3 tgetstr来查看该函数的使用帮助,如下所示,我们可以知道终端能力有3种,分别是布尔值、数字值和字符串值,而该函数是用来获取字符串值的终端能力。同时,我们也可以知道,这个函数是给使用termcap库的应用使用的,后台会转换为terminfo库中的值。termcap和terminfo都是描述终端能力的库,termcap出现的比较早,已经被terminfo取代,但为了兼容性,termcap的接口仍然保留。

在centos6中可以使用:cat /etc/termcap来查看所有终端的能力,/etc/termcap 是一个 ASCII 文件,这个文件在centos7和8中已不存在。在centos6/7/8中可以使用:infocmp来查看当前终端的能力,位于/usr/share/terminfo,terminfo数据库保存的是编译后的内容。关于终端能力的详细描述,可以参考这篇文章: termcap - 终端功能数据库 - 樊伟胜 - 博客园 。

这里引用一段tgetstr的使用说明(来源: The Termcap Library - Interrogate ):

tgetstr

Use tgetstr to get a string value. It returns a pointer to a string which is the capability value, or a null pointer if the capability is not present in the terminal description. There are two ways tgetstr can find space to store the string value:

You can ask tgetstr to allocate the space. Pass a null pointer for the argument area, and tgetstr will use malloc to allocate storage big enough for the value. Termcap will never free this storage or refer to it again; you should free it when you are finished with it. This method is more robust, since there is no need to guess how much space is needed. But it is supported only by the GNU termcap library.

You can provide the space. Provide for the argument area the address of a pointer variable of type char *. Before calling tgetstr, initialize the variable to point at available space. Then tgetstr will store the string value in that space and will increment the pointer variable to point after the space that has been used. You can use the same pointer variable for many calls to tgetstr. There is no way to determine how much space is needed for a single string, and no way for you to prevent or handle overflow of the area you have provided. However, you can be sure that the total size of all the string values you will obtain from the terminal description is no greater than the size of the description (unless you get the same capability twice). You can determine that size with strlen on the buffer you provided to tgetent. See below for an example. Providing the space yourself is the only method supported by the Unix version of termcap.

从下图中,我们可以看到第900行的area指向了buf,是上面英文提到的第2种用法,也即调用方分配好存储。

在terminal.c中加上打印来看看buf、area和tgetstr的值的变化情况:

char buf[TC_BUFSIZE];
printf("buf addr:%p\n", buf);

...

for (t = tstr; t->name != NULL; t++) {
	/* XXX: some systems' tgetstr needs non const */
	//terminal_alloc(el, t, tgetstr(strchr(t->name, *t->name),
	//    &area));
    char *tmp = tgetstr(strchr(t->name, *t->name), &area);
    printf("area:%p\n", area);
    printf("tgetstr ret val:%p\n", tmp);
    terminal_alloc(el, t, tmp);
}

打印结果如下:

buf addr:0x7ffe0ec93660

area:0x7ffe0ec93664(第1次for循环)

tgetstr ret val:0xec93660(第1次for循环)

可以发现第1次for循环tgetstr的返回值是buf被截断低4个字节后的值,按道理应该和buf的值一样,所以会产生内存非法访问的错误,导致segmentation fault。

问题到这里,令人百思不得其解,为什么就被截断了呢?

这时想起了编译时报的警告错误(写在文章开头): implicit declaration of function,这个警告是缺少函数原型声明导致的,也就是第一次编译的时候没有安装依赖的ncurse库,从而缺少头文件term.h,从而缺少tgetstr的函数原型声明。那么这个警告和函数返回值截断有没有关系呢?

通过下面的程序来测试一下,

foo.h

#ifndef __FOO_H__
#define __FOO_H__
void foo();
#endif

foo.c

#include <stdlib.h>
#include <stdio.h>
void foo()
{
  char buffer[1024];
  printf("buffer:%p\n", buffer);
  char *str = bar(buffer);
  printf("str:%p\n", str);
  printf("sizeof pointer:%d\n", sizeof(str));
  printf("sizeof int:%d\n", sizeof(int));
}

bar.c

#include <stdio.h>
char *bar(char *buffer)
{
  char *buf = buffer;
  printf("buf:%p\n", buf);
  return buf;
}

main.c

#include "foo.h"
int main(int argc, char *argv[])
{
  foo();
  return 0;
}

编译foo.c时报了如下警告implicit declaration of function,

$ gcc foo.c -c -o foo.o
foo.c: In function ‘foo’:
foo.c:7:15: warning: implicit declaration of function ‘bar’ [-Wimplicit-function-declaration]
   char *str = bar(buffer);
               ^~~
foo.c:7:15: warning: initialization of ‘char *’ from ‘int’ makes pointer from integer without a cast [-Wint-conversion]

执行结果:

$ ./main
buffer:0x7ffd563a8720
buf:0x7ffd563a8720
str:0x563a8720
sizeof pointer:8
sizeof int:4

通过输出结果不难看出,对于返回指针的bar()函数,其返回值被截断,只保留了低32位。这个问题也出现在: c - 64 bit function returns 32 bit pointer - Stack Overflow ,里面提到: By default all return values are int. So if a prototype is missing for function then compiler treats the return value as 32-bit and generates code for 32-bit return value. Thats when your upper 4 bytes gets truncated.

对于64位系统,由于int是4字节,指针是8字节,存在被截断的问题,容易导致程序crash,32位系统应该不存在该问题,所以在64位系统上要注意该编译警告带来的潜在问题,另外,就是养成一个良好的编译习惯,最好不要有警告。

解决方法

从该警告“implicit declaration of function”来看是由于缺少函数的原型声明,从man手册里知道tgetstr函数在term.h中有原型声明,只要terminal.c中include了这个头文件就可以了。在terminal.c中有如下代码,也就是说预编译if条件没成立。

 在目录:extra\libedit\libedit-20191231-3.1\src中打开CMakeLists.txt,发现有如下内容:

也就是说由于第一次make时没有安装ncurse依赖库,导致缺少term.h,导致HAVE_TERM_H没有被定义。所以怎么解决这个问题也就比较清楚了。

1.安装好ncurses库

sudo yum install -y ncurses-devel

2.清理cmake缓存:这一步很必要,如果不清理缓存,cmake还是会认为没有相应头文件term.h。进到相应的二进制目录,执行:

rm CMakeCache.txt; rm -rf CMakeFiles

3.重新cmake和make

不再出现编译警告“implicit declaration of function”,mysql输入用户名和密码后也不再crash了。

以上是我的解决方法,我们也来看看其他的处理方法:mysql segmentation fault_命令行登录mysql报Segmentation fault错误是怎么回事_一颗大球糖bobo的博客-CSDN博客,这种方法在网上能搜到很多,即:编辑文件terminal.c,把terminal_set方法中的 char buf[TC_BUFSIZE]; 这一行注释,再把 area = buf;改为 area = NULL;。

为什么这种方法可以呢?我们来分析一下。

这其实是那段tgetstr英文用法的第一种,由tgetstr函数自己去malloc一块足够大小的内存,tgetstr函数返回malloc出来的内存地址,那么同样的问题来了,编译警告“implicit declaration of function”会导致tgetstr函数返回值被截断为低4个字节,为什么mysql客户端此时不会crash呢?

这里涉及到linux系统中进程的地址空间里的堆(Heap)和栈(Stack)的概念,可以参考:Linux段管理,BSS段,data段,.rodata段,text段_开发之路-CSDN博客_rodata段 ,malloc分配出来的内存是放在堆上,terminal_set方法中的char buf[TC_BUFSIZE]是放在栈上。堆从进程的地址空间的低地址开始往高地址分配,栈从进程的地址空间的高地址开始往低地址分配。也就是说,堆的地址比栈的地址要低,我们来看一下打印出来的指针内容,

buf addr:0x7ffd8cc1e1a0

area:(nil)(第1次for循环)

tgetstr ret val:0x23e72fa(第1次for循环)

我们看到tgetstr函数的返回值0x23e72fa没有32位,也即证实了堆从进程的地址空间的低地址开始分配,既然没到32位,那么截断后的内容没有改变。

在mysql的官网,我们可以找到这个bug: MySQL Bugs: #58497: mysql client crashes due to unitialized string buffer in term.c 。好了,总结到此完毕。

  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值