GDB 的 break 在标签处的 break 处跳过行
GDB's break skips line on break at label
下面是一个hello world MIPS 汇编程序的调试会话。
该程序使用 GCC 进行汇编,并使用 gdb-multiarch 进行调试。
代码在QEMU上执行,GDB连接到8080上的QEMUs调试端口。
当执行 break main
时,我希望 GDB 在第 7 行中断 (jal hello
) 但它在第 9 行创建断点。
(gdb) file proj.out
Reading symbols from proj.out...done.
(gdb) target remote 127.0.0.1:8080
Remote debugging using 127.0.0.1:8080
0x00400290 in _ftext ()
(gdb) break main
Breakpoint 1 at 0x400460: file /import/src/main.s, line 9.
(gdb) list
1
2 .text
3 .globl main
4 .extern hello
5
6 main:
7 jal hello
8
9 li $a0, 0
10 li $v0, 4001
我可以为我添加到程序中的任意标签重现这个。仅在没有标签的情况下断开时不会发生这种情况。但是当使用 break main.s:6
而不是 break main
时也会发生这种情况。
我怀疑 GDB 坚持某种我不知道的约定。
程序版本:
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
mips-linux-gnu-gcc (Debian 4.3.5-4) 4.3.5
qemu-mips version 2.0.0 (Debian 2.0.0+dfsg-2ubuntu1.24)
operating system: ubuntu:14.04.4 docker container
编译命令:
mips-linux-gnu-gcc -g -static -mips32r5 -O0 -o
mips 架构有 "branch delay slots".
考虑一个简化的视图。 mips 有两个 独立单元:一个指令获取 单元和一个指令执行 单元。
获取单元运行"one ahead"执行单元。这允许单元重叠。也就是说,exec 单元能够与 fetch 并行操作。它执行在上一个周期中获取的 inst。
因此,在周期 0 中,提取了第一条指令。在周期 1 中,执行第一条指令,然后取出第二条指令。在周期 2 中,执行第 2 条指令,并取出第 3 条指令。这看起来像:
cycle fetch exec
0 1 n/a
1 2 1
2 3 2
3 4 3
在我们遇到任何类型的分支指令(即 jal
)之前,它工作正常。在您的示例中,我们有 7 jal hello
和 9 li $a0,0
。您没有显示您的 C 代码,但我怀疑 hello
有一个参数,而您的实际调用是 hello(0)
因此,在大多数拱门上,顺序为 li $a0,0
和 jal hello
。
因为指令获取运行 "one ahead",预取指令 在 之后 jal
将不得不被丢弃并被浪费。
因此,mips 具有分支延迟槽。指令 after 一个分支是 in 延迟槽。它总是执行,就像它出现在分支之前一样。
因此,从逻辑上讲,您的程序如下所示:
L1: li $a0,0 # first arg to hello
L2: jal hello # call to hello
L3: nop # branch delay slot
实际执行顺序为L1、L3、L2
编译器能够对此进行优化并在分支延迟槽中放置一条有用的指令:
L1: jal hello # call to hello
L2: li $a0,0 # first arg to hello
执行顺序为L2、L1。请记住,对于分支[taken or not],分支延迟槽中的指令总是always先执行,就好像它先来一样。
因此,gdb did 将断点放在正确的位置:在 main 的第一条指令上。但是,因为第一条指令是分支,所以放置 break
指令的正确位置是分支的分支延迟槽。
在您的示例中,jal
是第 7 行,它的分支延迟槽是第 9 行。
更新:
Unfortunately the breakpoint is set at the wrong position regardless of the instruction: I could replace jal hello
by li $a0, 1
and it wouldn't change anything.
很抱歉。 li
应该是一个线索,因为它是一个伪操作,它可以生成 1-2 条真实指令。例如,li $a0,0x01020304
将生成:lui $a0,0x0102 ori $a0,$a0,0x0304
但是,您可能仍然需要注意分支延迟槽。我不知道 qemu
,但一些 mips 模拟器,如 mars
或 spim
允许您配置插槽是否为 enabled/used [并且,对于它们,插槽默认 关闭]。如果关闭,则可以忽略插槽。否则,只需在每个分支后添加一个nop
。
The code is written "by hand" and is not compiled from C or any other language.
再次抱歉。我看到 "compiled with GCC" 而不是 "assembled with GCC"。
部分问题是 gdb
是高级语言源代码调试器。这是它的主要方向。它的行号概念是面向 HLL(例如 C)行号的。因此,在没有帮助的情况下,它可能难以映射 to/from asm 行号。即使来源是 .s
,也可能来自 cc -c -s -o foo.s foo.c ; cc -o foo foo.s
.
gdb
更喜欢用 -g
编译程序。这会添加某些 asm 指令来定义调试信息。要查看它是什么样的,请使用 C 程序 [或任何 .c
文件] 并使用 -g
[或 -gdwarf-2
] 和 -s
[交叉] 编译它。然后,查看输出.s
文件。
您可能需要在某些地方添加类似的指令来告诉gdb
您您认为行号应该是什么。当然,这可以手动完成。但是,众所周知,我采用给定的 .s
并通过 "metaprogramming" 脚本提供它以添加我需要的任何内容。所以,这个的输出就是被馈送到 gcc
--YMMV
但是,每当我使用 gdb
调试 asm 并需要精确控制时,我都会使用一些不同的 gdb 命令,这些命令更适合调试汇编程序。
stepi
而不是 step
。这一步通过单个 asm 指令而不是 gdb 认为 是源代码行。
disassemble main
而不是 list main
。这给出了实际的说明而不是源列表。或者 x/i <address>
。一个很好的例子是 x/i $pc
.
<address>
可以是标签,也可以是使用标签的简单表达式。
现在,一个大人物: 而不是 break <function>
或 break <line_number>
,我将使用 地址 形式:break *<address>
.
所以,如果 disassemble main
显示第一条指令在地址 0x00001000
,那么我会做 break *0x1000
。
但是,那会很乏味。地址形式允许符号。所以,你可以做break *main
。它还允许地址表达式:break *main+0x4
。我认为 "These are the droids you're looking for" :-)
另一种方法是考虑使用mars
或spim
进行模拟。它们是基于 GUI 的并且更容易使用(并且带有内置的汇编程序)。
如果您只是想学习 mips asm 并做一些简单的事情,那么从它们开始可能是更好的选择。我在 SO 上看到的大多数问题都使用它们或在真实硬件上进行调试 [通常在 linux] 下启动。
我没见过太多使用 qemu
的人。因此,如果您没有 OS 要求,mars/spim
可能值得一试。我都用过,我更喜欢 mars
根据您的项目[或将变得]有多大,它们可能仍然是其中一部分的答案(即用它们隔离和调试特定功能)。
如果您想尝试一下,这里有一个 link 火星:http://courses.missouristate.edu/KenVollmar/MARS/
下面是一个hello world MIPS 汇编程序的调试会话。 该程序使用 GCC 进行汇编,并使用 gdb-multiarch 进行调试。 代码在QEMU上执行,GDB连接到8080上的QEMUs调试端口。
当执行 break main
时,我希望 GDB 在第 7 行中断 (jal hello
) 但它在第 9 行创建断点。
(gdb) file proj.out
Reading symbols from proj.out...done.
(gdb) target remote 127.0.0.1:8080
Remote debugging using 127.0.0.1:8080
0x00400290 in _ftext ()
(gdb) break main
Breakpoint 1 at 0x400460: file /import/src/main.s, line 9.
(gdb) list
1
2 .text
3 .globl main
4 .extern hello
5
6 main:
7 jal hello
8
9 li $a0, 0
10 li $v0, 4001
我可以为我添加到程序中的任意标签重现这个。仅在没有标签的情况下断开时不会发生这种情况。但是当使用 break main.s:6
而不是 break main
时也会发生这种情况。
我怀疑 GDB 坚持某种我不知道的约定。
程序版本:
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
mips-linux-gnu-gcc (Debian 4.3.5-4) 4.3.5
qemu-mips version 2.0.0 (Debian 2.0.0+dfsg-2ubuntu1.24)
operating system: ubuntu:14.04.4 docker container
编译命令:
mips-linux-gnu-gcc -g -static -mips32r5 -O0 -o
mips 架构有 "branch delay slots".
考虑一个简化的视图。 mips 有两个 独立单元:一个指令获取 单元和一个指令执行 单元。
获取单元运行"one ahead"执行单元。这允许单元重叠。也就是说,exec 单元能够与 fetch 并行操作。它执行在上一个周期中获取的 inst。
因此,在周期 0 中,提取了第一条指令。在周期 1 中,执行第一条指令,然后取出第二条指令。在周期 2 中,执行第 2 条指令,并取出第 3 条指令。这看起来像:
cycle fetch exec
0 1 n/a
1 2 1
2 3 2
3 4 3
在我们遇到任何类型的分支指令(即 jal
)之前,它工作正常。在您的示例中,我们有 7 jal hello
和 9 li $a0,0
。您没有显示您的 C 代码,但我怀疑 hello
有一个参数,而您的实际调用是 hello(0)
因此,在大多数拱门上,顺序为 li $a0,0
和 jal hello
。
因为指令获取运行 "one ahead",预取指令 在 之后 jal
将不得不被丢弃并被浪费。
因此,mips 具有分支延迟槽。指令 after 一个分支是 in 延迟槽。它总是执行,就像它出现在分支之前一样。
因此,从逻辑上讲,您的程序如下所示:
L1: li $a0,0 # first arg to hello
L2: jal hello # call to hello
L3: nop # branch delay slot
实际执行顺序为L1、L3、L2
编译器能够对此进行优化并在分支延迟槽中放置一条有用的指令:
L1: jal hello # call to hello
L2: li $a0,0 # first arg to hello
执行顺序为L2、L1。请记住,对于分支[taken or not],分支延迟槽中的指令总是always先执行,就好像它先来一样。
因此,gdb did 将断点放在正确的位置:在 main 的第一条指令上。但是,因为第一条指令是分支,所以放置 break
指令的正确位置是分支的分支延迟槽。
在您的示例中,jal
是第 7 行,它的分支延迟槽是第 9 行。
更新:
Unfortunately the breakpoint is set at the wrong position regardless of the instruction: I could replace
jal hello
byli $a0, 1
and it wouldn't change anything.
很抱歉。 li
应该是一个线索,因为它是一个伪操作,它可以生成 1-2 条真实指令。例如,li $a0,0x01020304
将生成:lui $a0,0x0102 ori $a0,$a0,0x0304
但是,您可能仍然需要注意分支延迟槽。我不知道 qemu
,但一些 mips 模拟器,如 mars
或 spim
允许您配置插槽是否为 enabled/used [并且,对于它们,插槽默认 关闭]。如果关闭,则可以忽略插槽。否则,只需在每个分支后添加一个nop
。
The code is written "by hand" and is not compiled from C or any other language.
再次抱歉。我看到 "compiled with GCC" 而不是 "assembled with GCC"。
部分问题是 gdb
是高级语言源代码调试器。这是它的主要方向。它的行号概念是面向 HLL(例如 C)行号的。因此,在没有帮助的情况下,它可能难以映射 to/from asm 行号。即使来源是 .s
,也可能来自 cc -c -s -o foo.s foo.c ; cc -o foo foo.s
.
gdb
更喜欢用 -g
编译程序。这会添加某些 asm 指令来定义调试信息。要查看它是什么样的,请使用 C 程序 [或任何 .c
文件] 并使用 -g
[或 -gdwarf-2
] 和 -s
[交叉] 编译它。然后,查看输出.s
文件。
您可能需要在某些地方添加类似的指令来告诉gdb
您您认为行号应该是什么。当然,这可以手动完成。但是,众所周知,我采用给定的 .s
并通过 "metaprogramming" 脚本提供它以添加我需要的任何内容。所以,这个的输出就是被馈送到 gcc
--YMMV
但是,每当我使用 gdb
调试 asm 并需要精确控制时,我都会使用一些不同的 gdb 命令,这些命令更适合调试汇编程序。
stepi
而不是 step
。这一步通过单个 asm 指令而不是 gdb 认为 是源代码行。
disassemble main
而不是 list main
。这给出了实际的说明而不是源列表。或者 x/i <address>
。一个很好的例子是 x/i $pc
.
<address>
可以是标签,也可以是使用标签的简单表达式。
现在,一个大人物: 而不是 break <function>
或 break <line_number>
,我将使用 地址 形式:break *<address>
.
所以,如果 disassemble main
显示第一条指令在地址 0x00001000
,那么我会做 break *0x1000
。
但是,那会很乏味。地址形式允许符号。所以,你可以做break *main
。它还允许地址表达式:break *main+0x4
。我认为 "These are the droids you're looking for" :-)
另一种方法是考虑使用mars
或spim
进行模拟。它们是基于 GUI 的并且更容易使用(并且带有内置的汇编程序)。
如果您只是想学习 mips asm 并做一些简单的事情,那么从它们开始可能是更好的选择。我在 SO 上看到的大多数问题都使用它们或在真实硬件上进行调试 [通常在 linux] 下启动。
我没见过太多使用 qemu
的人。因此,如果您没有 OS 要求,mars/spim
可能值得一试。我都用过,我更喜欢 mars
根据您的项目[或将变得]有多大,它们可能仍然是其中一部分的答案(即用它们隔离和调试特定功能)。
如果您想尝试一下,这里有一个 link 火星:http://courses.missouristate.edu/KenVollmar/MARS/