2016-05-27 48 views
2

下面是hello world MIPS汇编程序的调试会话。该程序使用GCC进行组装,并使用gdb-multiarch进行调试。 代码在执行QEMU,GDB连接到QEMUs上8080GDB的break break标签跳线

当执行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 

回答

4

MIPS架构具有 “分支延迟槽”。

考虑一个简化的视图。 mips有两个分开单位:指令取指单位和指令执行单位。

提取单元在执行单元的“一个”之前运行。这允许单元的重叠。也就是说,执行单元能够与提取并行操作。它执行在前一周期获取的inst。

因此,在周期0中,取出第一条指令。在周期1中,执行第一条指令,取出第二条指令。在周期2中,执行2nd inst,并取出第3条指令。这看起来像:

cycle  fetch  exec 
0   1   n/a 
1   2   1 
2   3   2 
3   4   3 

这工作得很好,直到我们打任何形式(即jal)的一个分支指令。在你的例子中,我们有7 jal hello9 li $a0,0。你没有显示你的C代码,但我怀疑hello需要一个参数和实际电话是hello(0)

因此,顺序是li $a0,0和大多数拱门jal hello

由于取指运行“一前一后”,之后的预取指令将不得不被丢弃并且将被浪费。

所以,mips有分支延迟槽。指令的一个分支是的延时槽。它是总是执行,就好像它出现在分支之前一样。

所以,从逻辑上讲,你的程序是这样的:

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。请记住,对于分支[采取或而不是],分支延迟插槽中的指令始终是,总是执行,就像它先到达一样。

因此,gdb did把断点放在正确的位置上:在main的第一条指令上。但是,因为第一条指令是一个分支,所以放置指令break的正确位置是分支的分支延迟槽。

在您的例子中,是jal线7和它的分支延迟槽是线9


UPDATE:

不幸的是,断点设置在错误的位置不管指令如何:我可以用li $a0, 1代替jal hello,它不会改变任何东西。

对不起。 li应该是一个线索,因为它是一个伪操作,它可以生成1-2条真正的指令。例如,li $a0,0x01020304将生成:lui $a0,0x0102 ori $a0,$a0,0x0304

但是,您可能仍需注意分支延迟时隙。我不知道qemu,但一些mips模拟器如marsspim允许您配置是否启用/使用插槽[并且对于它们,插槽默认为off]。如果关闭,插槽可以被忽略。否则,只需在每个分支后添加一个nop即可。

该代码是“手动”编写的,不是从C或任何其他语言编译的。

再次,抱歉。我看到“用GCC编译”而不是“用GCC编译”。问题的


部分是gdb是一种高级语言源调试器。这是它的主要方向。其行号的概念面向HLL(例如C)行号。所以,如果没有一些帮助,它可能会难以映射到/从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并通过“元编程”脚本来添加我需要的任何东西。所以,这个输出是什么被送到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。我认为“这些你要找的机器人” :-)


另一种方法是考虑使用marsspim进行仿真。它们是基于GUI的,可以更容易使用(并带有内置汇编器)。

如果你只是想学习mips asm并做简单的事情,他们可能是更好的选择。我在SO上看到的大多数问题都使用它们,或者在真正的硬件上进行调试[通常在linux下启动]。

我还没有看到太多使用qemu。所以,如果你没有操作系统要求,mars/spim可能值得一试。我已经使用了两个,我更喜欢mars

取决于您的项目[或将成为]有多大,它们可能仍然是其中一部分的答案(即隔离和调试某个特定功能)。

如果您想尝试一下,这里是火星的链接:http://courses.missouristate.edu/KenVollmar/MARS/

+0

感谢您的广泛的答案。 不幸的是,无论指令如何,断点都被设置在错误的位置:我可以用'li $ a0,1'代替'jal hello',它不会改变任何东西。代码是“手工编写”的,不是从C或任何其他语言编译的。 –

+0

你的提示'break * main'是关键。 'dissassemble main'是相当有帮助的。非常感谢。 –