Linux程序崩溃分析(一)
引言
我们在做Linux开发时,常常会遇到程序崩溃的问题,这时会用gdb或者通过查看反汇编的方式去对程序进行分析,接下来,我们从底层的角度,去讲述如何分析程序崩溃的原因。
一、常见BUG
在进行分析前,先看看我总结归纳的常见BUG:
1.内存错误:
内存错误往往出现在使用了未分配的内存,或者没有及时释放分配的内存。
2.指针错误:
指针错误往往出现在使用了空指针,或者是指向的地址在函数返回后丢失,或者是偏移量出了问题,这个话题暂时不展开讨论。
3.判断条件出错:
比如 if (a == 1) 写成了 if (a=1),if (a && b) 写成了 if (a & b)。
4.参数未初始化
比较典型的就是申请结构体变量没对其进行memset,加之接口内部没做参数判断,从而传参导致接口异常。
5.未考虑字节序
在跨平台通信时要考虑字节序的问题,arm一般是小端字节序( little - endian ),x86一般是大端字节序( big - endian)。arm和x86进行通信时,要考虑到字节序问题。
6.线程同步错误
往往体现在共享数据上锁不当,导致线程死锁。
7.字节对齐错误
需要了解编译器对于字节对齐的默认属性,一般是4字节对齐,体现在结构体的设计方面。如果字节对齐做的不好,软件版本更新迭代会带来极大的隐患,通常做法是加上reserve字段。
8.配对操作没调用完全
比如open没有及时close,init没有及时release,从而导致资源的浪费。
二、从汇编角度分析C程序
我们在嵌入式开发中,使用arm架构居多,所以这里讨论的是arm架构的分析方案,如果用x86也可以用类似的思想。我们日常开发中遇到的程序崩溃,比较难查的问题往往是出现在内存访问部分,下面我们通过底层的汇编程序来讲述一段C语言程序,对于内存,硬件,是怎样一个执行流程。
1.arm汇编相关理论基础
下面我列举一些arm汇编的寄存器,并对其进行描述
常用的arm用户态寄存器如上表所示,有r0~r15这16个寄存器
r0~r3:通常在函数传参时使用(从左到右的顺序,大于4个参数时使用栈来传递)和返回值(r0通常被用作返回值)。在函数内部 r0-r3 也可以用来存储局部变量。
r4~r8,r10,r11:通常用来保存局部变量。r11通常用来作为(FP)栈基地址(下面会对这些概念进行讲述)
r12:可能在函数调用时被链接器使用,在函数内部,也可以存储局部变量。
r13:是SP寄存器,就是当前函数的栈顶指针。
r14:是LR寄存器,存放当前函数的返回地址。
r15:是PC寄存器,存放当前指令的地址。
上面讲述的FP,SP,LR,PC寄存器,它寄存器里面的内容是地址,这点不要混淆。
2.内存中的栈帧结构
刚刚我们提到了FP,SP,LR,PC寄存器,现在我们来展开聊聊这几个寄存器。
PC指针:刚刚提到PC指针里面存放着当前指令的地址,因为在我们arm架构,传统上是五级流水线,简单描述就是取址,然后取完代码是二进制,对它进行译码,翻译成各个动作,然后cpu参与计算,最后返回。PC指针就存放着当前指令的地址,扮演的角色就是告诉cpu需要访问的地址,也对应五级流水线中的取址操作。
SP指针:在函数申请变量的时候,会有一个动态压栈的过程,栈的大小会随着变量申请而逐渐增长,SP指针就指向你动态压栈所处在的地址。
FP指针:当前函数的起始地址。在函数调用时,进入另一个函数接口,也会进入另一个栈帧结构,里面会保存调用者的的起始地址(FP),用于出现问题时回溯,同时也有当前函数的起始地址(FP)。
LR指针:函数调用时,调用者的下一条指令地址。用于函数调用完返回时,可以进入下一条指令。
我们不妨先看看内存中的栈帧结构。
这就是内存中的栈帧结构,上图就是main函数在调用func1时的栈帧结构。绿色的是处在func1函数里面的,灰色的是在main函数里面的。
通过这张图,我们可以看出,在发生函数调用时,FP寄存器会指向当前函数调用的起始地址。在func1内部(绿色部分)还有一个FP,这个FP不是FP寄存器,而是内存中的数据,也表示地址。它指向调用者(main)的起始地址。它主要是在程序崩溃时,用来回溯上一级的PC LR SP FP的值,所以在调用时会保存上一个栈帧的数据,用来崩溃的时候一层一层回溯回去。
3.举例说明
在举例说明例子之前,先讲一下几个常用的汇编指令
mov:给某个寄存器赋值
/* 给r3寄存器赋值为8 */
mov r3, #8add:加法运算
/* r3 = r3 + 4 此处r3代表r3寄存器中的数据 */
add r3, r3, #4sub:减法运算
/* r3 = r3 - 4 此处r3代表r3寄存器中的数据 */
sub r3, r3, #4str:把寄存器的值写入内存
/* r3和fp是arm的寄存器,刚刚有所提及 * * 右边中括号里面的代表地址,代表fp指针指向的地址向下偏移8个字节 * * 这条的指令意思就是把r3寄存器的数据写入右边的地址. */
str r3, [fp, #-8] ldr:从内存中取数据到寄存器
/* r0和pc是arm的寄存器 刚刚有所提及 ** 右边中括号里面的代表地址,代表pc指针指向的地址向上偏移20个字节 ** 这条的指令意思就是从右边的地址指向的内存中取数据到r0寄存器. */
ldr r0, [pc, #20]bl:跳转到指定地址
/* 跳转到83ec地址,它是函数func_1的起始地址 */
bl 83ec <func_1>
下面我来写一段很简单的代码
#include <stdio.h>int func(int num1, int num2, int num3)
{int n = 0;n = num1 + num2;n = n + num3;return n;
}int main()
{int a = 0;int b = 1;a = func(1, 2, b);printf("value = %d\n", a);return 0;
}
翻译成反汇编(这里采用海思的交叉编译工具编译,然后通过objdump生成反汇编)
arm-himix200-linux-gcc test.c -o test
arm-himix200-linux-objdump -d test > debug.txt
反汇编中的C语言部分
00010410 <func>:/* 将调用者main的fp指针压入栈中,保存用于回溯 */10410: e52db004 push {fp} ; (str fp, [sp, #-4]!)/* fp的地址此处不做偏移,因为只压入一个数据 */10414: e28db000 add fp, sp, #0/* 将sp寄存器向低地址减28字节,其实这里是个开辟栈内存的动作 */10418: e24dd01c sub sp, sp, #28/* 将刚刚函数传参的r0寄存器的数据写入fp指针再向低地址偏移16字节的地址 */1041c: e50b0010 str r0, [fp, #-16]/* 将刚刚函数传参的r1寄存器的数据写入fp指针再向低地址偏移20字节的地址 */10420: e50b1014 str r1, [fp, #-20] ; 0xffffffec/* 将刚刚函数传参的r2寄存器的数据写入fp指针再向低地址偏移24字节的地址 */10424: e50b2018 str r2, [fp, #-24] ; 0xffffffe8/* 对应函数调用里面的int n = 0,将0直接赋值给r3寄存器 */10428: e3a03000 mov r3, #0/* 将r3寄存器的值写入fp指针再向低地址偏移8字节的地址 */1042c: e50b3008 str r3, [fp, #-8]/* 将fp指针再向低地址偏移16字节的地址的数据拿出来,读取到r2寄存器里,准备做加法运算 */10430: e51b2010 ldr r2, [fp, #-16]/* 将fp指针再向低地址偏移20字节的地址的数据拿出来,读取到r2寄存器里,准备做加法运算 */10434: e51b3014 ldr r3, [fp, #-20] ; 0xffffffec/* 将刚刚拿出来的两个数据做加法运算,对应函数调用里面的n = num1 + num2; */10438: e0823003 add r3, r2, r3/* 将刚刚算到的结果存回栈中 */1043c: e50b3008 str r3, [fp, #-8]/* 再从栈中把刚刚的数据读出来 */10440: e51b2008 ldr r2, [fp, #-8]/* 将fp指针再向低地址偏移24字节的地址的数据拿出来,读取到r2寄存器里,准备做第二个加法运算 */10444: e51b3018 ldr r3, [fp, #-24] ; 0xffffffe8/* 将刚刚拿出来的两个数据做加法运算,对应函数调用里面的n = n + num3; */10448: e0823003 add r3, r2, r3/* 将刚刚的计算的结果再写入栈内存中 */1044c: e50b3008 str r3, [fp, #-8]/* 从栈内存中取出数据放入r3寄存器 */10450: e51b3008 ldr r3, [fp, #-8]/* 将r3寄存器的数据转给r0寄存器,r0寄存器一般用来做返回值用 */10454: e1a00003 mov r0, r3/* 将sp指针归位,释放栈空间 */10458: e28bd000 add sp, fp, #0/* 将fp寄存器出栈 */1045c: e49db004 pop {fp} ; (ldr fp, [sp], #4)/* 跳转到lr寄存器所指向的地址,也就是函数调用的下一行 */10460: e12fff1e bx lr00010464 <main>: /* 运行到这里,栈帧已经生成,第一步push将fp, lr指针压入栈中 */10464: e92d4800 push {fp, lr}/* 因为刚刚那步同时push了两个值,fp和lr两个数据存放的地址要区分开来, ** sp代表当前动态压栈所处在的地址,所以将fp寄存器的地址向高地址偏移4个单位 */10468: e28db004 add fp, sp, #4/* 将sp寄存器的地址向高地址偏移8个单位,存放在fp,lr的后面 */1046c: e24dd008 sub sp, sp, #8/* 对应刚刚的int a = 0,这里把0赋值给r3寄存器 */10470: e3a03000 mov r3, #0/* 将r3寄存器的值写入内存,地址在fp指针指向的地址往低地址偏移8个单位(栈的生长方向是高地址到低地址) */10474: e50b3008 str r3, [fp, #-8]/* 对应刚刚的int b = 1,这里把1赋值给r3寄存器 */10478: e3a03001 mov r3, #1/* 将r3寄存器的值写入内存,地址在fp指针指向的地址往低地址偏移12个单位(栈的生长方向是高地址到低地址) */1047c: e50b300c str r3, [fp, #-12]/* 在刚刚变量b写入的地址中取出b的值,写入r2寄存器,这里r2将作为函数参数用 */10480: e51b200c ldr r2, [fp, #-12]/* 将2赋值给r1,这里r1将作为函数参数用 */10484: e3a01002 mov r1, #2/* 将1赋值给r1,这里r1将作为函数参数用 */10488: e3a00001 mov r0, #1/* 参数准备完毕,跳转到func函数,进行函数调用 */1048c: ebffffdf bl 10410 <func>/* 这边就是lr寄存器指向的地址,函数调用完,回到了这里.这里将函数的返回值存入fp指向的地址向低地址偏移8个字节的位置 */10490: e50b0008 str r0, [fp, #-8]/* 从刚刚存入的数据从内存中取出来,放入r1寄存器,准备调用printf函数 */10494: e51b1008 ldr r1, [fp, #-8]/* 将字符串读入r0寄存器,准备函数调用 */10498: e59f0010 ldr r0, [pc, #16] ; 104b0 <main+0x4c>/* 进入printf */1049c: ebffff85 bl 102b8 <printf@plt>/* 将r0 r3寄存器清0 */104a0: e3a03000 mov r3, #0/* 将r0 r3寄存器清0 */104a4: e1a00003 mov r0, r3/* 释放栈大小 */104a8: e24bd004 sub sp, fp, #4/* 将fp pc寄存器出栈 */104ac: e8bd8800 pop {fp, pc}104b0: 00010524 .word 0x00010524
已经写好了注释,可以通过注释从main函数开始一步一步看。
Linux程序崩溃分析(一)相关推荐
- 程序崩溃 分析工具_程序分析工具| 软件工程
程序崩溃 分析工具 A program analysis tool implies an automatic tool that takes the source code or the execut ...
- linux 内核部分崩溃,Linux 系统内核崩溃分析处理简介
Written by arstercz -2019-11-12 Linux 系统内核崩溃分析处理简介 背景说明 目前绝大多数的 Linux 发行版都会将 kdump.service 服务默认开启, 以 ...
- 暴雪团队使用VS进行Linux平台崩溃分析
蝎子 暴雪正在使用Visual Studio 2019在WSL上对Linux平台上发生的崩溃进行分析.本文来自暴雪高级软件工程师Bill Randolph,目前他负责暗黑破坏神4的开发工作.感谢Bil ...
- 记一次 .NET 某医疗器械 程序崩溃分析
一:背景 1.讲故事 前段时间有位朋友在微信上找到我,说他的程序偶发性崩溃,让我帮忙看下怎么回事,上面给的压力比较大,对于这种偶发性崩溃,比较好的办法就是利用 AEDebug 在程序崩溃的时候自动抽一 ...
- linux程序崩溃时调用链,Linux 获取并分析程序崩溃时的调用堆栈
下面是一个小例子,说明了程序出现段错误时,如何打印程序的堆栈信息. #include #include #include #include static void WidebrightSegvHand ...
- linux 程序crash 调试、原因分析及问题定位
目录标题 前言 core dump 开启core dump backtrace 静态库 动态库 最后补充几句 前言 linux 程序崩溃,如果能根据已有的插桩日志能排查出来自然好,但是往往日志未全覆盖 ...
- linux java缓存失效_转载:Linux服务器Cache占用过多内存导致系统内存不足最终java应用程序崩溃解决方案...
原文链接: https://blog.csdn.net/u014740338/article/details/66975550 问题描述 Linux内存使用量超过阈值,使得Java应用程序无可用内存, ...
- 在Linux中利用backtrace信息解决程序崩溃问题
转自:https://blog.csdn.net/jxgz_leo/article/details/53458366 一.导读 在程序调试过程中如果遇到程序崩溃死机的情况下我们通常多是通过出问题时的栈 ...
- Linux内存耗尽宕机6,转载:Linux服务器Cache占用过多内存导致系统内存不足最终java应用程序崩溃解决方案...
原文链接: https://blog.csdn.net/u014740338/article/details/66975550 问题描述 Linux内存使用量超过阈值,使得Java应用程序无可用内存, ...
最新文章
- python 字符串去重从小到大排列_110道题整理(1-60)
- 大数据,大格局,大发展
- QuartusII联合modelsim仿真时调用两个模块如何设置
- VMware虚拟机安装
- python程序设计基础第三版_Python程序设计(第三版)PPT及源码
- 为什么稀疏自编码器的正则项选用了相对熵(KL散度)的函数?
- 利用LR做性能测试中出现的常见问题解决方案
- oracle服务 ora_01033,Oracle ORA-01033 错误的解决办法
- 初识app之产品需求分析文档设计
- 【文献学习】《Reference-free detection of isolated SNPs》
- bios属于计算机软件系统吗,装系统必须知道的BIOS,到底是什么?
- 网页设计的步骤和标准都有哪些?
- Win10安装Kafka步骤
- linux 流量控制 1
- 安卓App太能乱来了!被曝一天扫你后台1.3万次:小米系统更新,一不小心扯出惊人真相...
- Linux-IO全整理:BIO/NIO/IO多路复用解析
- IntelliJ IDEA 13 皮肤/编辑器字体设置
- python滤波与图像去噪
- Chapter5 生长因子、受体和癌症
- Axure 9 实战案例,基本元件的应用 5,利用情形实现B站图文登录验证