C语言之编译和debug工具

发布于:2025-04-06 ⋅ 阅读:(25) ⋅ 点赞:(0)

gcc

gcc是GUN项目为C和C++提供的编译器

入门案例

gcc编译器最简单的使用案例:gcc hello.c -o hello,hello.c是源文件,-o参数指定了结果文件的名称

gcc命令的选项:

  • -v:打印编译细节
  • -E:仅仅进行预处理,把预处理结果输出到控制台
  • -S:编译源文件
  • -c:编译并汇编源文件
  • -o:指定输出文件
  • -I <dir>:指定某个文件夹作为搜索路径
  • -L:为gcc增加一个搜索链接库的目录

工作机制

一般高级语言的编译过程,可以分为预处理、编译、汇编、链接四个阶段。

预处理

预处理是编译过程中的第一步,处理各种预处理命令,包括头文件的包含(#include)、宏定义的扩展(#define)、条件编译的选择(#if #endif)等

打印出预处理的结果:gcc -E hello.c

源代码:hello.c

#include <stdio.h>

#define HELLO_LINUX "hello Linux!\n"
#define HELLO_MAC "hello MAC!\n"
#define HELLO_WINDOWS "hello Windows!\n"

int main(int argc, char const *argv[]) {

    #if defined(__linux__)
        printf(HELLO_LINUX);
    #elif defined(__APPLE__) || defined(__MACH__)
        printf(HELLO_MAC)
    #elif defined(__WIN32__)
        printf(HELLO_WINDOWS);
    #endif

    int a = 1;
    int b = 2;
    #if a > b
        printf("111\n");
    #else
        printf("222\n");
    #endif

    return 0;
}

打印出的结果:

# 1 "hello.c"
# 1 "<built-in>"
# 1 "<命令行>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<命令行>" 2
# 1 "hello.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 27 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/features.h" 1 3 4
# 375 "/usr/include/features.h" 3 4
# 1 "/usr/include/sys/cdefs.h" 1 3 4
# 392 "/usr/include/sys/cdefs.h" 3 4
# 1 "/usr/include/bits/wordsize.h" 1 3 4
# 393 "/usr/include/sys/cdefs.h" 2 3 4
# 376 "/usr/include/features.h" 2 3 4
# 399 "/usr/include/features.h" 3 4
# 1 "/usr/include/gnu/stubs.h" 1 3 4
# 10 "/usr/include/gnu/stubs.h" 3 4
# 1 "/usr/include/gnu/stubs-64.h" 1 3 4
# 11 "/usr/include/gnu/stubs.h" 2 3 4
# 400 "/usr/include/features.h" 2 3 4
# 28 "/usr/include/stdio.h" 2 3 4





# 1 "/usr/lib/gcc/x86_64-redhat-linux/4.8.5/include/stddef.h" 1 3 4
# 212 "/usr/lib/gcc/x86_64-redhat-linux/4.8.5/include/stddef.h" 3 4
typedef long unsigned int size_t;
# 34 "/usr/include/stdio.h" 2 3 4

# 1 "/usr/include/bits/types.h" 1 3 4
# 27 "/usr/include/bits/types.h" 3 4
# 1 "/usr/include/bits/wordsize.h" 1 3 4
# 28 "/usr/include/bits/types.h" 2 3 4


typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
typedef unsigned long int __u_long;


// 省略代码


int main(int argc, char const *argv[]) {


        printf("hello Linux!\n");






    int a = 1;
    int b = 2;



        printf("222\n");


    return 0;
}

从预处理的结果中观察预处理究竟做了什么:

  • 文件包含:#include 头文件:会把头文件整个复制到当前源码文件中
  • 宏展开:#define 宏名 常量:预处理会进行宏替换,把宏名提换为它所代表的常量
  • 条件编译:#if #endif:判断应该保留哪个分支,裁剪到多余的分支

编译

编译:编译器进行词法分析、语法分析,把源代码翻译成汇编语言。

打印出gcc编译器生成的汇编语言:gcc -S hello.c,会在当前目录下生成hello.s文件

生成的结果:

        .file   "hello.c"
        .section        .rodata
.LC0:
        .string "hello Linux!"
.LC1:
        .string "222"
        .text
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $32, %rsp
        movl    %edi, -20(%rbp)
        movq    %rsi, -32(%rbp)
        movl    $.LC0, %edi
        call    puts
        movl    $1, -4(%rbp)
        movl    $2, -8(%rbp)
        movl    $.LC1, %edi
        call    puts
        movl    $0, %eax
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"
        .section        .note.GNU-stack,"",@progbits

汇编

汇编:把汇编代码翻译成机器代码,即目标代码

查看生成的机器代码:

  • 第一种方式:gcc - c hello.s,自动生成hello.o文件
  • 第二种方式:as -o hello.o hello.s,-o参数指定生成的目标文件

查看生成的目标文件的文件类型:file hello.o,结果:

hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
ELF文件

链接

链接:将符号引用和符号定义替换为可执行文件的虚拟内存地址

总结

编译过程分为四步:预处理、编译、汇编、链接

  • 预处理:处理预处理命令,包括文件包含、宏定义、条件编译

    • 查看预处理的结果:gcc -E 源码文件,会把预处理的结果打印到屏幕上
  • 编译:把预处理的结果编译为汇编文件

    • 查看编译的结果:gcc -S 源文件,会在当前目录下生成.s文件,.s文件中存储了汇编代码
  • 汇编:把汇编代码编译为机器代码

    • 查看汇编结果:gcc -c 汇编代码,会在当前目录下自动生成.o文件,.o文件中存储了机器代码
  • 链接:将机器代码中的符号引用和符号定义替换为可执行文件的虚拟内存地址,最终生成可执行文件

Makefile

简介

Makefile:一个文本文件,并且文件名就叫"Makefile"。它包含了一系列指令,定义了整个项目的编译规则。

优点:自动化编译,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率

make命令:解释Makefile文件的命令

make命令的工作原则:

  • 一个目标文件,只有在它依赖的目标文件被更改后,它才会被重新编译。如果依赖文件的修改时间比目标文件的创建时间晚,证明依赖文件在目标文件创建后又进行了修改
  • make工具会遍历所有的依赖文件,并且把它们对应的目标文件进行更新。编译的命令和这些目标文件及它们对应的依赖文件的关系则全部储存在 Makefile 中。

入门案例

案例1

不使用Makefile的情况下对一个项目进行编译,在这里使用到了静态链接库和动态链接库

项目的源代码:

main.c:主程序的入口

#include <stdio.h>
#include "add_minus.h"
#include "multi_div.h"

int main(int argc, char const *argv[]) {
    printf("hello Cacu\n");

    int rst = 0;

    rst = add(3, 2);
    printf("3 + 2 = %d\n",rst);

    rst = minus(3, 2);
    printf("3 - 2 = %d\n",rst);

    rst = multi(3, 2);
    printf("3 * 2 = %d\n",rst);

    rst = div(3, 2);
    printf("3 / 2 = %d\n",rst);
    return 0;
}

add_minus.h:声明了加法和减法的接口

int add(int, int);
int minus(int, int);

multi_div.h:声明了乘法和除法的接口

int multi(int, int);
int div(int, int);

add_minus.c:实现了加法和减法的功能

#include "add_minus.h"

int add(int x, int y) {
    return x + y;
}

int minus(int x, int y) {
    return x - y;
}

multi_div.h:实现了乘法和除法的功能

#include "multi_div.h"

int multi(int x, int y) {
    return x * y;
}

int div(int x, int y) {
    return x / y;
}

从源代码到可执行文件的过程:

  • 编译源文件,生成目标文件:在这里分别编译3个".c"文件,生成".o"文件,".o"文件是目标文件,目标文件是二进制文件
    • gcc -c add_minus.c
    • gcc -c multi_div.c
    • gcc -c main.c
  • 链接目标文件,生成可执行文件:gcc -o main main.o add_minus.o multi_div.o,选项"-o"指定了目标文件的名称,这一步会把三个".o"文件链接为一个可执行文件
  • 清理:清理掉所有的目标文件:rm *.o

上面的案例演示了将源代码编译为可执行文件的基本流程,接下来还是以上面的源代码,演示静态链接库和动态链接库

  • 将每个.c文件都编译为目标文件:gcc -c main.c; gcc -c add_minus.c; gcc -c multi_div.c
  • 将add_minus.c编译为静态链接库:ar -r libadd_minus.a add_minus.o,ar命令用于生成静态链接库,-r 选项指定静态链接库的位置和名称,最好使用".o"文件来生成静态链接库,这样和平台是相关的。
  • 将multi_div.c编译为动态链接库:gcc multi_div.c -shared -o libmulti_div.so,-shared表示生成动态链接库,-o用于指定输出文件
  • 生成目标文件,同时连接静态库和动态库:gcc -o main main.o -L . -l multi_div -l add_minus,-L. 表示在当前目录下寻找库,-l指定库的名称,声明前缀lib和后缀.a、.so需要省略

案例2

最简单的Makefile文件,还是以案例1的代码作为源代码

main: main.c add_minus.c multi_div.c
	gcc -o main main.c add_minus.c multi_div.c

在控制台执行make命令,就会生成可执行文件main

语法

基本语法

Makefile文件的基本格式:

目标: 依赖
    命令   # 注意命令前必须是一个TAB键

make命令解析Makefile的工作机制:

  • 检测目标的依赖是否存在:生成目标前,会检查目标的依赖文件是否存在,如果不存在,会向下检索,检测是否有生成依赖的规则,如果没有,则报错
  • 检测目标是否需要更新:如果目标的依赖有一个被更新, 则目标会被更新

在上面的入门案例2中,可执行文件依赖所有的源文件,如果一个源文件被修改,那么所有的源文件都需要被重新编译,接下来对案例2进行优化:

main: main.o add_minus.o multi_div.o
	gcc -o main main.o add_minus.o multi_div.o

main.o: main.c
	gcc -c main.c

add_minus.o: add_minus.c
	gcc -c add_minus.c

multi_div.o: multi_div.c
	gcc -c multi_div.c

在当前Makefile文件中,每个源文件都是独立的,修改一个源文件,其它源文件不需要一同编译。

变量和模式

Makefile中支持定义变量,使用 “变量=值” 的形式来定义变量,使用 “${变量}” 的形式来引用变量

变量的类型:普通变量、自带变量、自动变量

普通变量:变量=值,定义完直接使用即可

自带变量:Makefile提供的内置变量,变量名大写

  • CC:编译器名称,例如:CC=gcc
  • CPPFLAGS: 预处理的选项
  • CFLAGS:编译器的选项
  • LDFLAGS:链接器的选项

自动变量:只能在命令中使用,Makefile根据上下文自动赋值的变量。

在学习自动变量的含义前,首先回顾一下Makefile的基本规则:

目标: 依赖
    命令   # 注意命令前必须是一个TAB键

自动变量是对于上述格式的简化:

  • $@: 表示目标
  • $<: 表示第一个依赖
  • $^: 表示所有依赖, 组成一个列表, 以空格隔开, 如果这个列表中有重复的项则消除重复项。

模式:%,表示一个或多个字符,目标和依赖必须同时使用 %,表示它们有相同的名称。比如: main.o:main.c,可以写为 %.o: %.c

使用变量和模式来优化入门案例:

target=main
object=main.o add_minus.o multi_div.o

CC=gcc
CPPFLAGS=-I ./

${target}:${object}
	${CC} -o $@ $^

%.o: %.c
	${CC} -o $@ -c $^ ${CPPFLAGS}

函数

makefile中的函数有很多,在这里学习两个最常用的。

wildcard:查找指定目录下的指定类型的文件

  • 案例:src=$(wildcard *.c),找到当前目录下所有后缀为.c的文件,赋值给src,等价于src=main.c fun1.c fun2.c

patsubst:匹配替换

  • 案例:obj=$(patsubst %.c, %.o, $(src)),把src变量里所有后缀为.c的文件替换成.o,等价于obj=main.o fun1.o fun2.o

使用函数来优化入门案例:

target=main

src=${wildcard ./*.c}
object=${patsubst %.c, %.o, ${src}

CC=gcc
CPPFLAGS=-I ./

${target}:${object}
	${CC} -o $@ $^

%.o: %.c
	${CC} -o $@ -c $^ ${CPPFLAGS}

makefile清理操作

清理编译过程中产生的中间文件,make会把makefile里出现的第一个target当作缺省target。其他的除非是生成缺省target需要,不会执行。

Makefile中的clean命令通常不会被执行,需要用户手动执行make clean命令,来删除编译过程中产生的 “.o” 文件

clean命令的案例:

clean
    rm -f *.o

实战案例

案例1:

target=abook

src=${wildcard ./*.c}                # 查找当前目录下所有的.c文件
object=${patsubst %.c, %.o, ${src}}  # 将.c替换为.o

CC=gcc
CPPFLAGS= -Wall -Wextra -pedantic -std=c99   # 指定编译时的参数

${target}:${object}                 # $@ 目标  $^  全部参数
        ${CC} -o $@ $^

%.o: %.c
        ${CC} -o $@ -c $^ ${CPPFLAGS}

clean:                # 需要执行 make clean命令,如果删掉.o文件,make命令会重新执行
        rm -f *.o

在命令行执行make && make clean命令

GDB

GDB:GNU Debugger,Linux下的调试工具,可以调试C、Java、PHP等语言。

初步使用:

  • 在命令行输入gdb,即可进入gdb提供的交互式界面
  • 在交互式界面输入help,即可查看gdb提供的帮助文档

入门案例

程序的源代码:gdb3.c

#include <stdio.h>

int main(int argc, char const *argv[]) {
    int a = 0;
    printf("请输入一个数字:");
    scanf("%d", &a);
    printf("%d的平方:%d\n", a, a * a);
    return 0;
}

编译:gcc -g gdb3.c -o gdb3,使用gdb调试程序,在编译时,必须要加上-g参数。

调试:

  • 进入gdb的交互式界面:gdb
  • 加载程序:file 可执行文件
  • 设置断点:break gdb3.c:4
  • 查看源代码:list
  • 运行程序:run,遇到断点后会自动停下
  • 单步执行:step,遇到用户的自定义函数,会进入自定义函数。在这里,单步执行时,如果程序卡住,表示用户需要在控制台输入数据
  • 查看变量:print 变量
  • 继续向下执行:continue,遇到断点会停下

被调试的程序编译时必须加入-g参数

使用gdb调试的C程序,在编译时,必须加上 -g 参数,加上调试信息,-g选项的作用是在可执行文件中加入源代码的信息,比如可执行文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证gdb能找到源文件。

判断文件是否带有调试信息:`readelf -S 可执行程序 | grep debug

交互式界面下的基本命令

运行程序:

  • run(简写r): 运行程序,当遇到断点后,程序会在断点处停止运行,等待用户输入下一步命令
  • start:启动程序并立即停止在程序的入口处
  • continue(简写c) : 继续执行,到下一个断点停止(或运行结束)
  • next(简写n) : 单步跟踪程序,当遇到函数调用时,也不进入此函数体;此命令同step的主要区别是,step遇到用户自定义的函数,将步进到函数中去运行,而next则直接调用函数,不会进入到函数体内。
  • step (简写s):单步调试如果有函数调用,则进入函数;与命令n不同,n是不进入调用的函数的
  • until(简写u):可以运行程序直到退出循环体
  • until+行号: 运行至某行,不仅仅用来跳出循环
  • finish: 运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址和返回值及参数值等信息。
  • call 函数(参数):调用程序中可见的函数,并传递“参数”,如:call gdb_test(55)
  • quit(简写q) : 退出gdb

设置断点:

  • break 文件名:n (简写b n):在指定文件的第n行处设置断点
  • b fn1 if a>b:条件断点设置
  • break func(break缩写为b):在函数func()的入口处设置断点,如:break cb_button
  • delete 断点号n:删除第n个断点
  • disable 断点号n:暂停第n个断点
  • enable 断点号n:开启第n个断点
  • clear 行号n:清除第n行的断点
  • info b (info breakpoints) :显示当前程序的断点设置情况
  • delete breakpoints:清除所有断点:

查看源代码:

  • list :简记为 l ,其作用就是列出程序的源代码,默认每次显示10行。
  • list 行号:将显示当前文件以“行号”为中心的前后10行代码,如:list 12
  • list 函数名:将显示“函数名”所在函数的源代码,如:list main
  • list :不带参数,将接着上一次 list 命令的,输出下边的内容。

打印表达式

  • print 表达式(简记p): 其中“表达式”可以是任何当前正在被测试程序的有效表达式,比如当前正在调试C语言的程序,那么“表达式”可以是任何C语言的有效表达式,包括数字,变量甚至是函数调用。
    • print a:将显示整数 a 的值
    • print ++a:将把 a 中的值加1,并显示出来
    • print name:将显示字符串 name 的值
    • print gdb_test(22):将以整数22作为参数调用 gdb_test() 函数
    • print gdb_test(a):将以变量 a 作为参数调用 gdb_test() 函数
  • display 表达式:在单步运行时将非常有用,使用display命令设置一个表达式后,它将在每次单步进行指令后,紧接着输出被设置的表达式及值。如: display a
  • watch 表达式:设置一个监视点,一旦被监视的“表达式”的值改变,gdb将强行终止正在被调试的程序。如: watch a
  • whatis :查询变量或函数
  • info function: 查询函数
  • info locals: 显示当前堆栈页的所有变量

查询运行信息

  • where/bt :当前运行的堆栈列表
  • bt backtrace 显示当前调用堆栈
  • up/down 改变堆栈显示的深度
  • set args 参数:指定运行时的参数
  • show args:查看设置好的参数
  • info program: 来查看程序的是否在运行,进程号,被暂停的原因。

使用经验

在上面的章节中,学习了gdb的基本知识,在这里,结合平时对于gdb的使用,总结一些常用的操作

查看函数调用栈中的栈帧

  • breaktrace命令,简写为bt
  • frame命令:简写为f,参数是一个数字,代表当前函数调用栈中第几个栈帧,栈帧由下到上从1开始编码。f 1,查看当前函数调用栈内main函数的情况
  • where命令:查看当前函数调用栈

当前函数内局部变量的值

info locals

让程序一直运行到从当前函数返回为止

finish命令

修改变量的值

set var 变量 = 值

监控某个变量的值

  • display命令:监控某个变量的值,程序每运行一行,自动显示该变量的值
  • undisplay命令:取消监控

启动程序时向程序中传递参数

第一种方式:在命令行中直接传递参数:可以在GDB命令行中使用run命令后面跟上程序的参数

gdb ./my_program
(gdb) run arg1 arg2

第二种方式:使用set args命令设置参数

gdb ./my_program
(gdb) set args arg1 arg2
(gdb) run

调试时指定跳到第几行

jump 行号,需要先在该行设置断点

debug模式下无法获取用户输入

在使用gdb进行调试时,gdb默认是运行在交互模式(interactive mode)下的,而交互模式会导致gdb无法正确地获取用户的输入。这是因为gdb会将标准输入重定向到自己的输入流,而不是终端。

解决这个问题的一种方法是将gdb设置为非交互模式(non-interactive mode),这样gdb就能够正确地获取用户的输入。可以在调试程序之前,在gdb中使用以下命令来设置非交互模式:

set pagination off
set non-stop on

这样设置后,gdb会禁用分页输出(pagination)和停止在非关键点(non-stop mode),从而允许程序在调试过程中正常运行,而不会暂停等待输入。

容易混淆的知识点

start和run的区别

start和run是两个用于启动程序的命令,它们有一些区别和不同的使用场景

  • start命令:start命令用于启动程序并立即停止在程序的入口处,然后等待调试器的进一步指令。它可以让你在程序刚刚开始执行时暂停,以便设置断点或进行其他调试操作。在使用start命令后,你可以使用continue命令继续程序的执行,或者使用其他GDB调试命令进行进一步的调试操作。
  • run命令:run命令用于启动程序并从程序的起始位置开始连续执行。它会一直执行程序,直到遇到断点、程序结束或其他调试终止条件。如果你不需要在程序刚开始执行时暂停,而是希望直接开始连续执行程序,则可以使用run命令。

常用操作

在程序执行过程中监视某个变量的改变

watch命令

执行完当前方法后返回到上一个方法

finish命令

程序中有fork函数,想要debug子进程,该怎么办?

在程序开始执行之前,输入命令 set follow-fork-mode child,表示fork后将进入子进程调试,子进程调试完成,输入命令 detach 脱离子进程,然后,set follow-fork-mode parent,设置模式为调试父进程。

调试指定线程

info threads:列出当前程序中的线程信息
thread <thread_number>:切换到指定线程

valgrind

用于检测内存泄漏的工具

安装:yum install -y valgrind

使用:valgrind --leak-check=full --show-leak-kinds=all -s <程序>,通过valgrind来执行程序,执行完成后,会显示程序中出现的内存错误。-s参数用于打印出检测到的错误