gcc、 binutils、gdb

本文深入探讨了GNU编译器集合(GCC)、Binutils工具集和GDB调试器的功能与应用。详细讲解了gcc编译选项、自定义程序入口、32位程序编译方法;Binutils中的addr2line、ar、nm、size、strings、objdump工具的使用;以及GDB的启动方式、断点调试、函数调用栈查看等高级调试技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1、gcc编译器

2、Binutils工具集

3、调试利器GDB


1、gcc编译器

GCC与gcc的不同

GNU计划:  GNU's Not Unix,Unix的收费所以模仿Unix界面和使用方式发起的一个计划,目标是创建一套完全自由的操作系统

GCC(GNU Compiler Collection):GNU编译器集合,包含众多语言的编译器(C,C++,Java,D,Objective-C,etc.)

gcc:特指GCC中的C语言编译器

gcc关键编译选项

   - 预处理指令: gcc -E file.c -o file.i

   - 编译指令: gcc -S file.i -o file.s

   - 汇编指令: gcc -c file.s -o file.o

   - 生成映射文件: gcc -Wl,-Map=test.map file.c

   - 宏定义: gcc -D'TEST="test"' file.c

   - 获取系统头文件路径: gcc -v file.c

   - 获取目标的完整依赖关系: gcc -M test.c

   - 获取目标的部分依赖关系: gcc -MM test.c

   - 指定库文件及库文件搜索路径

          ★ -L 选项:指定库文件的搜索路径

          ★ -l 选项:指定库文件:  gcc test.c -L. -lfunc

   - 指定头文件路径 : gcc -Idir test.c

//func.h
#include <stdio.h>
void func()
{
#ifdef TEST
    printf("TEST = %s\n", TEST);
#endif
    return;
}
//test.c
#include <stdio.h>
#include "func.h"
int g_global = 0;
int g_test = 1;
int main(int argc, char *argv[])
{
    func();
    
    printf("&g_global = %p\n", &g_global);
    printf("&g_test = %p\n", &g_test);
    printf("&func = %p\n", &func);
    printf("&main = %p\n", &main);
	
    return 0;
}

image

image

imageimage.gif

imageimage.gif

func.h改名func.c, 去除test.c的文件包含

image

自定义程序入口函数

gcc提供 -e 选项用于在链接时指定入口函数(ld的-e选项),自定义入口函数时必须使用 -nostartfiles 选项进行链接

    - -nostartfiles:Do not use the standard system startup files when linking.  使用-nostartfiles即告诉链接器不再使用_start()函数作为代码段入口函数

    - -nodefaultlibs

    - -nostdlib

64位的Ubuntu系统配上默认的额64位的gcc,如何编译32位程序

 - sudo apt-get install gcc-multilib g++-multilib module-assistant

 - gcc -m32 test.c

 

2、Binutils工具集

GNU为GCC编译器提供了配套的辅助工具集(Binutils))

http://www.gnu.org/software/binutils/

addr2line:将指定地址转换为对应的文件名行号,常用于分析和定位内存访问错误的问题

//func.c
#include <stdio.h>

int* g_pointer;//0

void func()
{
    *g_pointer = (int)"D.T.Software"; // 对0地址处赋值, 段错误, 如果有50万行代码如何快速定位错误?

    return;
}
//test.c
#include <stdio.h>

int g_global = 0;
int g_test = 1;

extern int* g_pointer;
extern void func();

int main(int argc, char *argv[])
{
    printf("&g_global = %p\n", &g_global);
    printf("&g_test = %p\n", &g_test);
    printf("&g_pointer = %p\n", &g_pointer);
    printf("g_pointer = %p\n", g_pointer);
    printf("&func = %p\n", &func);
    printf("&main = %p\n", &main);
    
    func();
	
    return 0;
}

addr2line 示例:定位0地址访问

1. 开启core dump选项(程序奔溃的最后一刻记录内存信息,寄存器信息等)

ulimit -c unlimited

2. 运行程序,并生成崩溃时的 core文件

执行导致程序崩溃的测试用例

3. 读取 core文件,获取IP寄存器的值(如0x08048000

dmesg core

4. 使用addr2line定位代码行

addr2line 0x08048000 -f -e test.out

                几乎所有的调试辅助工具都依赖于目标文件中的调试信息,调试信息的运用能够快速定位问题

strip:剔除程序文件中的调试信息,减少目标程序的大小

     - 一般在程序发布前都需要将调试信息剔除,过多的调试信息可能影响程序的执行效率

ar

     - 打包目标文件  ar crs libname.a x.o y.o

     - 解压目标文件  ar x libname.a

nm

     - 列出目标文件中的标识符(变量名,函数名),输出结果由三部分组成:{标识符对应地址,标识符所在标识符名字}

段标识说明

size:获取目标文件中的所有段大小

strings:获取目标文件中的所有字符串常量

objdump

     - 反汇编目标文件,查看汇编到源码的映射

objdump -h的输出说明

区分VMA与LMA

VMA表示Virtual Memory Address,即虚拟地址,

LMA表示Load Memory Address,即加载地址

正常情况下这两个值应该是一样的,但是在有些嵌入式系统中,特别是在那些程序放在ROM的系统中时,LMA和VMA是不相同的

 

3、调试利器GDB

GDB

      - GNU项目中的调试器(gnu debuger),能够跟踪程序的执行,也能够恢复程序崩溃前的状态

      - binutils工具集属于静态分析工具,目标是可执行程序文件(事后分析),GDB是动态分析工具,目标是进程

GDB的启动方式

      - 直接启动:gdb、gdb test.out、gdb test.out core

      - 动态连接:gdb test.out pid // 动态跟踪这一进程

GDB的常规应用

      - 自定义程序的启动方式(指定影响程序运行的参数如:命令行参数)

      - 设置条件断点(在条件满足时暂停程序的执行)

      - 回溯检查导致程序异常结束的原因(core dump)

      - 动态改变程序执行流(定位问题的辅助方式)

实验分析

fun.c

#include <stdio.h>

int* g_pointer;

void func()
{
    *g_pointer = (int)"D.T.Software";

    return;
}
#include <stdio.h>
#include <unistd.h>

extern int* g_pointer;
extern void func();

void test_1()
{
    printf("test_1() : %p\n", test_1);
}

void test_2()
{
    printf("test_2() : %p\n", test_2);
}

void test_3()
{
    printf("test_3() : %p\n", test_3);
}

int main(int argc, char *argv[])
{
    typedef void(TFunc)();
    TFunc* fa[] = {test_1, test_2, test_3};
    int i = 0;
    
    printf("main() : begin...\n");
    
    for(i=0; i<argc; i++)
    {
        printf("argv[%d] = %s\n", i, argv[i]);
    }
    
    for(i=0; i<100; i++)
    {
        fa[i%3]();
        sleep(argc > 1); // 休眠1s
    }

    printf("g_pointer = %p\n", g_pointer);

    func();
    
    printf("main() : end...\n");

    return 0;
}

编译运行出现段错误,使用gdb调试分析错误

调试1

 直接定位了错误之处,比静态分析工具强大多

调试2

演示设置命令行参数

演示gdb动态链接到一个进程

使用GDB 进行断点调试

断点类型

      - 软件断点:由非法指令异常实现(软件实现),软件断点适用于运行于内存中的程序

      - 硬件断点:由硬件特性实现(数量有限),硬件断点适用于运行于Flash中的程序

      - 数据断点:由硬件特性实现(数量有限), 数据断点用于监视一段内存,若这段内存被访问(被读,被写)程序立即停下

软件断点的相关操作

      - 通过函数名设置断点

                 break func_name [ if var = value ]

                 tbreak func_name [ if var = value ]  (break设置的断点总是有效的,tbreak设置一次有效断点,若指明了条件就为条件断点)

      - 通过文件名行号设置断点

                 break file_name:line_num [ if var = value ]

                 tbreak file_name:line_num [ if var = value ]

 

硬件断点及其应用

      - 当代码位于只读存储器(Flash)时,只能通过硬件断点调试,硬件断点需要硬件支持,数量有限

      - GDB中通过 hbreak 命令支持硬件断点,hbreak 与 break 使用方式完全一致

实验分析

使用gdb进行断点调试

可以尝试跳过43行func()的调用,若程序执行正常确定func函数有问题

接下来进行第二次调试

第三次调试

整个过程没有修改源代码...就成功定位错误,并解决问题

GDB中支持数据断点的设置

      - watch 命令用于监视变量是否被改变(本质为硬件断点)

      - watch命令的用法:watch var_name

GDB中的内存查看

      - GDB中可以检查任意内存区域中的数据

      - 命令语法:x /Nuf expression

              N : 需要打印的单元数

              u : 每个单元的大小

               f :  数据打印的格式

 

 

示例:判断系统大小端

实验分析

变量断点和内存查看 test.c

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

int g_var = 0;

void* thread_func(void* args)
{
    sleep(5);
    
    g_var = 1;
}

int main()
{
    int i = 0;
    pthread_t tid = 0;
    
    pthread_create(&tid, NULL, thread_func, NULL);
    
    for(i=0; i<10; i++)
    {
        printf("g_var = %d\n", g_var);
        
        sleep(1);
    }
}

下面调试分析 定位哪一行代码修改了全局变量

这样就可以得知11行代码改写了数据

函数调用栈的查看(backtrace和frame)

      - backtrace   查看函数调用的顺序(函数调用栈的信息)

      - frame N      切换到栈编号为N的上下文中

      - info frame   查看当前函数调用的栈帧信息

实验分析

函数调用栈的查看 frame.c

#include <stdio.h>


int sum(int n)
{
    int ret = 0;
    
    if( n > 0 )
    {
        ret = n + sum(n-1);
    }
    
    return ret;
}


int main()
{
    int s = 0;
    
    s = sum(10);
    
    printf("sum = %d\n", s);
    
    return 0;
}

下面调试分析

调试中的小技巧

实验分析

#include <stdio.h>

int g_var = 1;

struct ST
{
    int i;
    int j;
};

int func()
{
    struct ST st[5] = {0};
    int i = 0;
    
    for(i=0; i<5; i++)
    {
        st[i].i = i;
        st[i].j = i * i;
    }
    
    for(i=0; i<5; i++)
    {
        printf("st[%d].i = %d\n", i, st[i].i);
        printf("st[%d].j = %d\n", i, st[i].j);
    }
}

int main()
{
    static c_var = 2;
	
    func();
	
    return 0;
}

演示断点处自动打印 

演示符号查看

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值