掘金 后端 ( ) • 2024-04-28 11:15

python出的core,栈都是c语言的栈,没办法从core中知道core在了python代码的哪一行,导致无从下手分析定位问题,搜索了一下相关资料,没有特别系统的分析方法,所以整理一下分析python core的方法以及一些可能的原因总结

core是什么?

linux下进程异常崩溃退出时,可以生成一份当时的内存、寄存器等的文件镜像,也就是一个core文件,我们可以用gdb命令分析core文件,查看当时的代码栈、内存等信息

检查是否开启core

  1. 使用 ulimit -a 查看第一行 core file size,如果是0就代表没有开,需要在/etc/profile中加一行ulimit -c unlimited,然后source /etc/profile,当前及后续登录的shell都会开启core
  2. 修改/etc/sysctl.conf中kernel.core_pattern = /home/core.%e.%p.%t,修改core的生成路径,sysctl -p使配置生效

环境准备

实测yum install安装的python不可用,需要编译一个新的python,编译时指定--with-pydebug参数。

加--with-pydebug参数后,编译的时候gcc会带-g参数,有调试信息,该参数同样会启用python的一些检查机制,方便我们编码时提前发现问题。具体的参数内容可见官方文档: https://docs.python.org/zh-cn/3.12/using/configure.html#debug-build

  • 操作系统: linux centos8
  • python版本: python3.8

下载python解释器源码

下载地址:https://www.python.org/ftp/python/3.8.8/Python-3.8.8.tar.xz

编译python

编译前需要下载一些依赖的工具

yum install zlib-devel bzip2-devel ncurses-devel sqlite-devel readline-devel tk-devel gcc make libffi-devel

然后开始编译

tar -xvf Python-3.8.8.tar.xz
cd Python-3.8.8
./configure --with-pydebug
make

编译完成后,会在当前目录生成一个python二进制文件,就是我们想要的python解释器了。

查看core的python栈

生成一个core

首先写一个python脚本来生成一个core。脚本通过访问0地址来生成core

from ctypes import c_int

print(c_int.from_address(0).value)

执行./python generate_core.py来执行脚本,会生成一个core。(生成core的路径可以通过查看操作系统的/proc/sys/kernel/core_pattern查看)

分析core

使用gdb工具来打开一个core文件,然后使用bt命令打印栈信息

gdb python_path(编译的python二进制的绝对路径) core_path(core文件的绝对路径) 

image.png

可以看到栈信息全是c的栈,然后载入使用Python源码中自带的gdb工具,键入

python import sys; sys.path.append("/root/python3.8/Python-3.8.8/Tools/gdb"); import libpython

使用py-bt命令打印python栈信息

py-bt

image.png 可以看到对应的core的python栈了,接下来就可以根据业务逻辑代码,去分析为什么会产生core。

常见的产生core的原因

常见的产生core的情况是有以下几种

Segmentation faule(段错误),这时进程的退出码是-11

1、访问非法指针

上面的生成core的示例脚本就是一个访问非法指针的例子,在64位操作系统中,指针的大小是8个字节,常见的指针地址一般前两个字节是0x00, 第三个字节是0x7f, 后面的是随机的。如果访问非法指针,那么就会取不到值引发信号11段错误,导致出core。

image.png 可以看到这个core的原因是memcpy函数访问了空指针导致的core。

2、调用非法函数(也等同于访问非法指针,只是现象不同)

示例代码,该代码定义了一个函数指针,然后创建了一个指向空的函数指针,调用该函数

#include<stdio.h>
typedef int(*func)(int, int);
int main()
{
    func add_func = NULL;
    add_func(1, 2);
    return 0;
}

编译运行该文件

gcc test.c
./a.out

image.png 会生成一个core,可以看到core原因是段错误

然后gdb解析core image.png 这种的core,栈后面的函数是??,找不到地址对应的函数符号名,前面的栈地址指针也是非法的,这个是一个空指针

abort,系统主动abort,这时进程的退出码是-6

一般是os的检测,多次free同一块内存可以触发

示例代码

#include<stdio.h>
#include <stdlib.h>
int main()
{
    char *name = (char *)malloc(8);
    free(name);
    free(name);
    return 0;
}

编译运行(编译时在使用gcc -g test.c 添加调试信息)

image.png

可以看到core的原因是Aborted,同时前面提示了double free。

image.png 由于添加了调试信息,gdb的时候可以看到哪一行代码调用的free函数引发的abort

主动产生core的方法

示例代码很简单

import time
while True:
    print('hello world!!!')
    time.sleep(1)

不想影响当前进程的执行,又想保留一个core现场

使用gcore 进程pid 的命令,会生成一个core

gcore $pid

image.png

到系统的对应目录下找这个core文件使用gdb解析即可分析

主动core并且停止当前进程的运行

使用kill命令向进程发信号6(abort)或者11(segmentation fault)即可

kill -6 $pid

例如 kill -6 1742424

image.png