Linux编程入门二调试

该篇主要简要介绍linux下常用的一些调试的工具。参考自徐晓鑫 后台开发核心技术与应用实践

strace

所有操作系统在其内核都有一些内建的函数,这些函数可以用来完成一些系统级别的功能,一般称Linux系统上的这些函数为“系统调用”(system call)。这些函数代表了用户空间到内核空间的一种转换。应用程序不能直接访问Linux内核,也不能直接调用内核函数。应用程序可以跳转到system_call的内核位置,内核会检查系统调用号,这个号码会告诉内核进程正在请求哪种服务。然后,它查看系统调用表,找到所调用的内核函数入口地址,调用该函数,然后返回到进程。
strace是通过跟踪系统调用来让开发者知道程序在后台所做事情的工具。

使用strace

输入g++ -o test test.cpp编译,得到可执行文件test。

#include 
using namespace std;
int main()
{
        int a;
        cin >> a;
        cout << a << endl;
        return 0;
}

用strace调用执行(strace ./test),输出如下图所示:
Linux编程入门二调试_第1张图片Linux编程入门二调试_第2张图片
每一行都是一次系统调用,等号左边是系统调用的函数名及其参数,右边是该调用的返回值。 从strace结果可以看到,系统首先调用execve,以开始一个新的进程,接着进行一些环境的初始化操作(装载被执行程序,载入libc函数库,设置内存映射等),最后停顿在read(0,处,这也就是执行到了cin函数后,等待用户输入数字。在输入数字8后,再调用write函数将格式化后的数值8输出到屏幕,最后调用exit_group退出进程。
Linux编程入门二调试_第3张图片
详细分析

execve("./test", ["./test"], [/* 22 vars */]) = 0

对于命令行下执行的程序,execve(或exec系列调用中的某一个)均为strace输出系统调用中的第一个。strace首先调用fork或clone函数新建一个子进程,然后在子进程中调用exec载入需要执行的程序。

brk(0) = 0xa2a000

以0作为参数调用brk,返回值为内存管理的起始地址(若在子进程中调用malloc,则从该地址开始分配空间)。

mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ff0301ad000

使用mmap函数进行匿名内存映射,以此获取4096 bytes内存空间,该空间起始地址0x7ff0301ad000匿名内存映射就是为了不涉及具体文件名,避免了文件的创建及打开,这种只能用于具有亲缘关系的进程间通信。

access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)

调用access函数检验/etc/ld.so.preload是否存在。

open("/etc/ld.so.cache", O_RDONLY) = 3

调用open函数尝试打开/etc/ld.so.cache文件,返回文件描述符为3。

fstat(3, {st_mode=S_IFREG|0644, st_size==75853, ...}) = 0

使用fstat函数获取/etc/ld.so.cache文件信息

mmap(NULL, 75853, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7ff03019a000

调用mmap函数将/etc/ld.so.cache文件映射至内存

close(3)

close关闭文件描述符为3指向的/etc/ld.so.cache文件。

open("/usr/lib64/libstdc++.so.6", O_RDONLY) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360ce*9\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=989840, ...}) = 0
mmap(0x392a600000, 3166648, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x392a600000

调用open和read,从/usr/lib64/libstdc++.so.6该lib库文件中读取832 bytes,即读取ELF头信息。(ELF头部中有进程的进入点,这里就是在获得进程的进入点)

mprotect(0x392a6e8000, 2097152, PROT_NONE) = 0 

使用mprotect函数对0x392a6e8000,起始的2097152 bytes空间进行保护(PROT_NONE参数就是不能访问,对应还有PROT_READ表示可以读取)。

munmap(0x7ff03019a000, 75853)

调用munmap函数,将/etc/ld.so.cache文件从内存中去映射。

read函数读取从终端输入的内容,write函数输出内容到终端

read(0, 输入内容
"输入内容\n", 1024) = 2
write(1, "输入内容\n", 28) = 2

用strace来跟踪信号传递

先输入命令“strace ./test”,等待的时候不要输入任何东西,然后打开另外一个窗口,输入命令"killall test"。strace中的结果显示test进程"+++ killed by SIGTERM ++"。

统计系统调用

通过使用参数-c,可以将进程所有的系统调用做一个统计分析并返回。
Linux编程入门二调试_第4张图片

追踪现有进程

参数-p可以追踪现有的进程,具体如下:strace -p pid

Valgrind

valgrind是一套Linux下的开放源代码的仿真调试工具的集合。valgrind由内核以及基于内核的其他调试工具组成。内核类似于一个框架,它模拟一个CPU环境,并提供服务给其他工具;而其他工具则类似于插件,利用内核提供的服务完成各种特定的内存调试任务。
valgrind包括如下一些工具:

  • Memcheck: 重量级的内存检查器,能够发现开发中绝大多数内存错误使用情况,比如:使用未初始化的内存,使用已经释放了的内存,内存访问越界等。
  • Callgrind: 和gprof类似的分析工具,但它对程序的运行观察更是入微,能提供更多的信息。和gprof不同,它不需要在编译源代码时附加特殊选项,但推荐加上调试选项。Callgrind收集程序运行时的一些数据,建立函数调用关系图,还可以有选择地进行Cache模拟。在运行结束时,它会把分析数据写入一个文件。callgrind_annotate可以把这个文件的内容转化成可读的形式。
  • Cachegrind: 它主要用来检查程序中缓存使用出现的问题。Cache分析器,它模拟CPU中的一级缓存I1,D1和二级缓存,能够精确地指出程序中Cache的丢失和命中。如果需要,它还能够为用户提供Cache丢失次数、内存引用次数以及每行代码、每个函数、每个模块及整个程序产生的指令数。
  • Helgrind: 它主要用来检查多线程程序中出现的竞争问题。Helgrind寻找内存中被多个线程访问,而又没有一贯加锁的区域,这些区域往往是线程之间失去同步的地方。
  • Massif: 堆栈分析器,它能测量程序在堆栈中使用了多少内存,告诉我们堆块、堆管理块和栈的大小。Massif能帮助我们减少内存的使用,在带有虚拟内存的现代系统中,它还能够加速程序的运行,减少程序停留在交换区中的几率。
  • Extension: 可以利用Core提供的功能,自己编写特定的内存调试工具。

访问非法内存

#include 
#include 
using namespace std;
void func()
{
        int *x = (int *)malloc( 10 * sizeof(int));
        x[10] = 0;
}
int main()
{
        func();
        cout << "done" << endl;
        return 0;
}

用g++ -g -o test test.cpp命令编译之后,./test执行后输出done。
valgrind的参数分为两类:一类是core的参数,它对所有的工具都适用;另外一类就是具体某个工具如Memcheck的参数。valgrind默认的工具就是Memcheck,也可通过"–tool = tool name"指令指定其他的工具。valgrind提供了大量的参数满足用户特定的调试需求,具体可参考其用户手册。

Memcheck能够检测出内存问题,关键在于其建立了两个全局表。Valid-Value表:对于进程的整个地址空间中的每个字节都有与之对应的8bit;对于CPU的每个寄存器,也有一个与之对应的bit向量。这些bit负责记录该字节或者寄存器值是否具有有效的、已初始化的值。Valid-Address表:对于进程整个地址空间中的每个字节,还有与之对应的1bit,负责记录该地址是否能够被读写。检测原理:当要读写内存中某个字节时,首先检查这个字节对应的A bit。如果该bit显示该位置是无效位置,memcheck则报告读写错误。内核(core)类似于一个虚拟的CPU环境,这样当内存中的某个字节被加载到真实的CPU中时,该字节对应的V bit也被加载到虚拟的CPU环境中。一旦寄存器中的值,被用来产生内存地址,或者该值能够影响程序输出,则memcheck会检查对应的V bit,如果该值尚未初始化,则会报告使用未初始化内存错误。

Linux编程入门二调试_第5张图片
这个例子使用Memcheck,左边显示类似行号的数字(22042)表示的是进程id。Invalid write of size 4说明这是一个对内存的非法写操作,非法写操作的内存是4 bytes。发生错误时的函数堆栈,具体的源代码是第7行。
这个程序有2个问题:fun函数中动态申请的堆内存没有释放,对堆内存的访问越界。

使用未初始化内存

#include 
using namespace std;
int main()
{
        int a[5];
        int i, s = 0;
        a[0]=a[1]=a[3]=a[4]=0;
        for(i=0;i<5;i++)
                s = s + a[i];
        if(s==33)
                cout << "sum is 33" << endl;
        else
                cout << "sum is not 33" << endl;
        return 0;
}

用g++ -g -o test test.cpp命令编译,使用memcheck模块来分析内存使用情况:

Linux编程入门二调试_第6张图片
结果显示在第10行,程序的跳转依赖于一个未初始化的变量。

内存读写越界

内存读写越界是指访问了没有权限访问的内存地址空间,比如访问数组时越界、对动态内存访问时超出了申请的内存大小范围。

#include 
#include 
using namespace std;
int main()
{
        int len = 4;
        int *pt = (int *)malloc(len*sizeof(int));
        int *p = pt;
        for(int i=0;i

Linux编程入门二调试_第7张图片
输出结果显示在该程序的第11行,进行了非法的写操作,在第12行,进行了非法读操作。

内存覆盖

C语言可以直接操作内存且C标准库中提供了大量这样的函数,比如strcpy、strncpy、memcpy、strcat等。这些函数有一个共同的特点就是需要设置源地址和目标地址,且src和dst指向的地址不能发生重叠,否则结果将不可预期。

#include 
#include 
#include 
int main()
{
        char x[50];
        int i;
        for(i=0;i<50;i++)
                x[i] = i+1;
        strncpy(x+20,x,20);
        strncpy(x+20,x,21);
        strncpy(x,x+20,20);
        strncpy(x,x+20,21);
        x[39]='\0';
        strcpy(x,x+20);
        x[39] = 39;
        x[40] = '\n';
        strcpy(x,x+20);
        return 0;
}

在15与17行中,src和dst所指向的地址相差20,但指定的复制长度却是21,这样就会把之前的值覆盖。第24行程序类似。
Linux编程入门二调试_第8张图片

动态内存管理错误

常见的内存分配方式分3种:静态存储、栈上分配、堆上分配。常用的内存动态分配函数包括:malloc、alloc、realloc、new等,动态释放函数包括free和delete等。一旦成功申请了动态内存,就需要自己对其进行内存管理,常见内存动态管理错误如下:

  • 申请和释放不一致:用malloc/alloc/realloc申请,用free释放;用new申请,用delete释放。
  • 申请和释放不匹配:申请多少就需释放多少
  • 释放后仍然读写
#include 
#include 
int main()
{
        int i;
        char *p = (char *)malloc(10);
        char *pt = p;
        for(i = 0; i < 10; i++)
        {
                p[i]='z';
        }
        delete p;
        pt[1]='x';
        free(pt);
        return 0;
}

Linux编程入门二调试_第9张图片
直接执行此程序,处于coredump状态。释放无效内存会出现coredump,非法读写内存不一定会出现coredump。

Linux编程入门二调试_第10张图片
第12行分配和释放函数不匹配,13行发生非法写操作,也就是往释放后的内存地址写值,14行释放内存函数非法。

内存泄漏

内存泄漏指的是在程序中动态申请内存,在使用完后没有释放,程序的其他部分也无法访问该内存。

main函数调用mk函数生成树结点,在调用完后,没有调用相应的函数释放内存,这样内存中的这个树结构无法被其他部分访问,造成内存泄漏。
tree.h代码

#ifndef _TREE_
#define _TREE_
typedef struct _node{
        struct _node *l;
        struct _node *r;
        char v;
}node;
node *mk(node *l, node *r, char val);
void nodefr(node *n);
#endif

tree.c

#include 
#include "tree.h"
node *mk(node *l, node *r, char val)
{
        node *f = (node *)malloc(sizeof(*f));
        f->l = l;
        f->r = r;
        f->v = val;
        return f;
}
void nodefr(node *n)
{
        if(n)
        {
                nodefr(n->l);
                nodefr(n->r);
                free(n);
        }
}

test.cpp

#include 
#include "tree.h"
int main()
{
        node *tree1, *tree2, *tree3;
        tree1 = mk(mk(mk(0,0,'3'),0,'2'),0,'1');
        tree2 = mk(0,mk(0,mk(0,0,'6'),'5'),'4');
        tree3 = mk(mk(tree1,tree2,'8'),0,'7');
        return 0;
}

makefile编写

test: test.o tree.o
        g++ -g -o test test.o tree.o
tree.o: tree.cpp tree.h
        g++ -g -c tree.cpp -o tree.o
test.o: test.cpp
        g++ -g -c test.cpp -o test.o

Linux编程入门二调试_第11张图片
memcheck将内存泄漏分为两种:一种是可能的内存泄漏(possibly lost),另一种是确定的内存泄漏(definitely lost)。可能的内存泄漏是指仍然存在某个指针能够访问某块内存,但该指针指向的已经不是该内存首地址。确定的内存泄漏是指已经不能够访问这块内存。确定的内存泄漏又分两种:直接的(direct)和间接的(indirect)。直接和间接的区别就是,直接是没有任何指针指向该内存,间接是指指向该内存的指针都位于内存泄漏处。在这个例子中,跟节点是直接泄漏,而其他节点是间接泄漏。

你可能感兴趣的:(linux编程)