linux 调试c(linux打开软件命令)
其实linux 调试c的问题并不复杂,但是又很多的朋友都不太了解linux打开软件命令,因此呢,今天小编就来为大家分享linux 调试c的一些知识,希望可以帮助到大家,下面我们一起来看看这个问题的分析吧!
在Linux下如何开发C程序
在Linux开发环境下,GCC是进行C程序开发不可缺少的编译工具。GCC是GNU C Compile的缩写,是GNU/Linux系统下的标准C编译器。虽然GCC没有集成的开发环境,但堪称是目前效率很高的C/C++编译器。《linux就该这么学》非常值得您一看。Linux平台下C程序开发步骤如下:
1.利用编辑器把程序的源代码编写到一个文本文件中。
比如编辑test.c程序内容如下:
/*这是一个测试程序*/
#include<stdio.h>
int main(void)
{
printf("Hello Linux!");
}
2.用C编译器GCC编译连接,生成可执行文件。
$gcc test.c
编译完成后,GCC会创建一个名为a.out的文件。如果想要指定输出文件,可以使用选项-o,命令如下所示:
$gcc-o test1 test.c
这时可执行文件名就变为test1,而不是a.out。
3.用C调试器调试程序。
4.运行该可执行文件。在此例中运行的文件是:
$./a.out或者 test1
结果将得出:
Hello Linux!
除了编译器外,Linux还提供了调试工具GDB和程序自动维护工具Make等支持C语言编程的辅助工具。如果想要了解GCC的所有使用说明,使用以下命令:
$man gcc
C语言调试的作用C语言调试器是如何工作的
C语言调试的作用,C语言调试器是如何工作的很多人还不知道,现在让我们一起来看看吧!
C语言调试器是如何工作的
当你用GDB的时候,可以看到它完全控制了应用程序进程。当你在程序运行的时候用 Ctrl+ C,程序的运行就能够终止,而GDB能展示它的当前地址、堆栈跟踪信息之类的内容。你知道C语言调试器是如何工作的吗?下面是小编为大家带来的关于C语言调试器是如何工作的的知识,欢迎阅读。
但是它们怎么不工作呢?
开始,让我们先研究它怎样才会不工作。它不能通过阅读和分析程序的二进制信息来模拟程序的运行。它其实能做,而那应该能起作用(Valgrind内存调试器就是这样工作的),但是这样的话会很慢。Valgrind会让程序慢1000倍,但是GDB不会。它的工作机制与Qemu虚拟机一样。
所以到底是怎么回事?黑魔法?……不,如果那样的话就太简单了。
另一种猜想?……?破解!是的,这里正是这样的。操作系统内核也提供了一些帮助。
首先,关于Linux的进程机制需要了解一件事:父进程可以获得子进程的附加信息,也能够ptrace它们。并且你可以猜到的是,调试器是被调试的进程的父进程(或者它会变成父进程,在Linux中进程可以将一个进程变为自己子进程:-))
Linux Ptrace API
Linux Ptrace API允许一个(调试器)进程来获取低等级的其他(被调试的)进程的信息。特别的,这个调试器可以:
读写被调试进程的内存:PTRACE_PEEKTEXT、PTRACE_PEEKUSER、PTRACE_POKE……
读写被调试进程的CPU寄存器 PTRACE_GETREGSET、PTRACE_SETREGS
因系统活动而被提醒:PTRACE_O_TRACEEXEC, PTRACE_O_TRACECLONE, PTRACE_O_EXITKILL, PTRACE_SYSCALL(你可以通过这些标识区分exec syscall、clone、exit以及其他系统调用)
控制它的执行:PTRACE_SINGLESTEP、PTRACE_KILL、PTRACE_INTERRUPT、PTRACE_CONT(注意,CPU在这里是单步执行)
修改它的信号处理:PTRACE_GETSIGINFO、PTRACE_SETSIGINFO
Ptrace是如何实现的?
Ptrace的实现不在本文讨论的范围内,所以我不想进一步讨论,只是简单地解释它是如何工作的(我不是内核专家,如果我说错了请一定指出来,并原谅我过分简化:-))
Ptrace是Linux内核的一部分,所以它能够获取进程所有内核级信息:
读写数据?Linux有copy_to/from_user。
获取CPU寄存器?用copy_regset_to/from_user很轻松(这里没有什么复杂的,因为CPU寄存器在进程未被调度时保存在Linux的struct task_struct*调度结构中)。
修改信号处理?更新域last_siginfo
单步执行?在处理器出发执行前,设置进程task结构的right flag(ARM、x86)
Ptrace是在很多计划的操作中被Hooked(搜索 ptrace_event函数),所以它可以在被询问时(PTRACE_O_TRACEEXEC选项和与它相关的),向调试器发出一个SIGTRAP信号。
没有Ptrace的系统会怎么样呢?
这个解释超出了特定的Linux本地调试,但是对于大部分其他环境是合理的。要了解GDB在不同目标平台请求的内容,你可以看一下它在目标栈里面的操作。
在这个目标接口里,你可以看到所有C调试需要的高级操作:
struct target_ops
{
struct target_ops*beneath;
/* To the target under this one.*/
const char*to_shortname;
/* Name this target type*/
const char*to_longname;
/* Name for printing*/
const char*to_doc;
/* Documentation. Does not include trailing
newline, and starts with a one-line descrip-
tion(probably similar to to_longname).*/
void(*to_attach)(struct target_ops*ops, const char*, int);
void(*to_fetch_registers)(struct target_ops*, struct regcache*, int);
void(*to_store_registers)(struct target_ops*, struct regcache*, int);
int(*to__breakpoint)(struct target_ops*, struct gdbarch*,
struct bp_target_info*);
int(*to__watchpoint)(struct target_ops*,
CORE_ADDR, int, int, struct expression*);
}
普通的GDB调用这些函数,然后目标相关的组件再实现它们。(概念上)这是一个栈,或者一个金字塔:栈顶的是非常通用的,比如:
系统特定的Linux
本地或远程调试
调试方式特定的(ptrace、ttrace)
指令集特定的(Linux ARM、Linux x86)
那个远程目标很有趣,因为它通过一个连接协议(TCP/IP、串行端口)把两台“电脑”间的执行栈分离开来。
那个远程的部分可以是运行在另一台Linux机器上的'gdbserver。但是它也可以是一个硬件调试端口的界面(JTAG)或者一个虚拟的机器管理程序(比如 Qemu),并能够代替内核和ptrace的功能。那个远程根调试器会查询管理程序的结构,或者直接地查询处理器硬件寄存器来代替对OS内核结构的查询。
想要深层次学习这个远程协议,Embecosm写了一篇一个关于不同信息的详细指南。Gdbserver的事件处理循环在这,而也可以在这里找到Qemu gdb-server stub。
总结一下
我们能看到ptrace的API提供了这里所有底层机制被要求实现的调试器:
获取exec系统调用并从调用的地方阻止它执行
查询CPU的寄存器来获得处理器当前指令以及栈的地址
获取clone或fork事件来检测新线程
查看并改变数据地址读取并改变内存的变量
但是这就是一个调试器的全部工作吗?不,这只是那些非常低级的部分……它还会处理符号。这是,链接源程序和二进制文件。被忽视可能也是最重要的的一件事:断点!我会首先解释一下断点是如何工作的,因为这部分内容非常有趣且需要技巧,然后回到符号处理。
断点不是Ptrace API的一部分
就像我们之前看到的那样,断点不是ptrace API的一部分。但是我们可以改动内存并获取被调试的程序信号。你看不到其中的相关之处?这是因为断点的实现比较需要技巧并且还要一点hack!让我们来检验一下如何在一个指定的地址设置一个断点。
1、这个调试器读取(ptrace追踪)存在地址里的二进制指令,并保存在它自己的数据结构中。
2、它在这个位置写入一个不合法的指令。不管这个指令是啥,只要它是不合法的。
3、当被调试的程序运行到这个不合法的指令时(或者更准确地说,处理器将内存中的内容设置好时)它不会继续运行(因为它是不合法的)。
4、在现代多任务系统中,一个不合法的指令不会使整个系统崩溃掉,但是会通过引发一个中断(或错误)把控制权交回给系统内核。
5、这个中断被Linux翻译成一个SIGTRAP信号,然后被发送到处理器……或者发给它的父进程,就像调试器希望的那样。
6、调试器获得信号并查看被调试的程序指令指针的值(换言之,是陷入 trap发生的地方)。如果这个IP地址是在断点列表中,那么就是一个调试器的断点(否则就是一个进程中的错误,只需要传过信号并让它崩溃)。
7、现在,那个被调试的程序已经停在了断点,调试器可以让用户来做任何他/她想要做的事,等待时机合适继续执行。
8、为了要继续执行,这个调试器需要 1、写入正确的指令来回到被调试的程序的内存; 2、单步执行(继续执行单个CPU指令,伴随着ptrace单步执行); 3、把非法指令写回去(使得这个执行过程下一次可以再次停止);4、让这个执行正常运行
很整洁,是不是?作为一个旁观的评论,你可以注意到,如果不是所有线程同时停止的话这个算法是不会工作的(因为运行的线程可能会在合法的指令出现时传出断点)。我不会详细讨论GDB是如何解决这个问题的,但在这篇论文里已经说得很详细了:使用GDB不间断调试多线程程序。简要地说,他们把指令写到内存中的其他地方,然后把那个指令的指针指向那个地址并单步执行处理器。但是问题在于一些指令是和地址相关的,比如跳转和条件跳转……
处理符号和调试信息
现在,让我们回到信号和调试信息处理。我没有详细地学习这部分,所以只是大体地说一说。
首先,我们是否可以不使用调试信息和信号地址来调试呢?答案是可以。因为正如我们看到过的那样,所有的低级指令是对CPU寄存器和内存地址来操作的,不是源程序层面的信息。因此,这个到源程序的链接只是为了方便用户。没有调试信息的时候,你看程序的方式就像是处理器(和内核)看到的一样:二进制(汇编)指令和内存字节。GDB不需要进一步的信息来把二进制信息翻译成CPU指令:
(gdb) x/10x$pc# heXadecimal representation
0x402c60: 0x56415741 0x54415541 0x55f48949 0x4853fd89
0x402c70: 0x03a8ec81 0x8b480000 0x8b48643e 0x00282504
0x402c80: 0x89480000 0x03982484
(gdb) x/10i$pc# Instruction representation
=> 0x402c60: push%r15
0x402c62: push%r14
0x402c64: push%r13
0x402c66: push%r12
0x402c68: mov%rsi,%r12
0x402c6b: push%rbp
0x402c6c: mov%edi,%ebp
0x402c6e: push%rbx
0x402c6f: sub$0x3a8,%rsp
0x402c76: mov(%rsi),%rdi
现在,如果我们加上调试信息,GDB能够把符号名称和地址配对:
(gdb)$pc
$1=(void(*)()) 0x402c60
你可以通过 nm-a$file来获取ELF二进制的符号列表:
nm-a/usr/lib/debug/usr/bin/ls.debug| grep" main"
0000000000402c60 T main
GDB还会能够展示堆栈跟踪信息(稍后会详细说),但是只有感兴趣的那部分:
(gdb) where
#0 write()
#1 0x0000003d492769e3 in _IO_new_file_write()
#2 0x0000003d49277e4c in new_do_write()
#3 _IO_new_do_write()
#4 0x0000003d49278223 in _IO_new_file_overflow()
#5 0x00000000004085bb in print_current_files()
#6 0x000000000040431b in main()
我们现在有了PC地址和相应的函数,就是这样。在一个函数中,你将需要对着汇编来调试!
现在让我们加入调试信息:就是DWARF规范下的gcc-g选项。我不是特别熟悉这个规范,但我知道它提供的:
地址到代码行和行到地址的配对
数据类型的定义,包括typedef和structure
本地变量和函数参数以及它们的类型
$ dwarfdump/usr/lib/debug/usr/bin/ls.debug| grep 402ce4
0x00402ce4 [1289, 0] NS
$ addr2line-e/usr/lib/debug/usr/bin/ls.debug 0x00402ce4
/usr/src/debug/coreutils-8.21/src/ls.c:1289
试一试dwarfdump来查看二进制文件里嵌入的信息。addr2line也能用到这些信息:
很多源代码层的调试命令会依赖于这些信息,比如next命令,这会在下一行的地址设置一个断点,那个print命令会依赖于变量的类型来输出(char、int、float,而不是二进制或十六进制)。
最后总结
我们已经见过调试器内部的好多方面了,所以我只会最后说几点:
这个堆栈跟踪信息也是通过当前的帧是向上“解开(unwinded)”的($sp和$bp/#fp),每个堆栈帧处理一次。函数的名称和参数以及本地变量名可以在调试信息中找到。
监视点(<code>watchpoints)是通过处理器的帮助(如果有)实现的:在寄存器里标记哪些地址应该被监控,然后它会在那内存被读写的时候引发一个异常。如果不支持这项功能,或者你请求的断点超过了处理器所支持的……那么调试器就会回到“手动”监视:一个指令一个指令地执行这个程序,并检查是否当前的操作到达了一个监视点的地址。是的,这很慢!
反向调试也可以这样进行,记录每个操作的效果,并反向执行。
条件断点是正常的断点,除非在内部,调试器在将控制权交给用户前检查当前的情况。如果当前的情况不满足,程序将会默默地继续运行。
还可以玩gdb gdb,或者更好的(好多了)gdb--pid$(pid of gdb),因为把两个调试器放到同一个终端里是疯狂的:-)。还可以调试系统:
qemu-system-i386-gdb tcp::1234
gdb--pid$(pidof qemu-system-i386)
gdb/boot/vmlinuz--exec"target remote localhost:1234"
Linux环境下使用VScode调试CMake工程
在本文中,我们将探讨如何在Linux环境下使用VSCode对基于CMake的工程进行编译和调试。首先,对于C++编译和相关工具如g++、gdb的初学者,可以参考前面的教程以建立基础理解。
CMake的作用在于优化大型C++项目的编译流程。它能管理复杂的文件结构,处理依赖关系,使得原本冗长的编译命令变得简洁。以一个包含多个文件夹和源文件的工程为例,CMake能生成编译指令,降低繁琐程度。
在演示的CMake工程目录中,build文件夹用于存放编译中间文件,而源代码文件夹中包含了项目的核心内容。若在终端使用CMake编译,步骤是直接在build目录下运行cmake和make命令。
在VSCode中,配置CMake编译的过程包括创建tasks.json文件,其中包含了cmake和make的命令。执行build任务就等于执行了这两个命令,实现了CMake的编译。
接下来,调试CMake工程就变得简单了。编译完成后,VSCode会自动识别生成的可执行文件helloCMake。在launch.json中,需要配置使用gdb调试器,指定要调试的文件和断点位置。只需在helloCMake.cpp文件中设置断点,通过F5键即可启动调试。
总的来说,通过VSCode和CMake的结合,即使在Linux环境中,管理和调试C++项目也变得更加直观和高效。