问题

在Loongson 3A3000环境中,使用qemu的用户态模式,执行x86 32位(i386)的HelloWorld程序,直接崩溃。

复现步骤如下:

  1. HelloWorld程序示例:

     #include <stdio.h>
    	
     int main(void) {
         printf("Hello World!\n");
     }
    
  2. 在i386环境中编译此程序:

     gcc --static hello.c -o hello
    
  3. 将hello程序拷贝到Loongson 3A3000的环境中。

  4. 在Loongson 3A3000环境中编译qemu,生成目标qemu-i386。具体操作方式为:到龙芯官网下载其维护的qemu最新版本,解压到环境中,进入解压后的目录,执行如下命令进行编译:

     ./configure --target-list=i386-linux-user --disable-kvm --disable-xen 
     make
    

完成后,在i386-linux-user/目录中会生成qemu-i386程序。

  1. 使用qemu-i386执行hello程序,示例如:

     i386-linux-user/ /home/hello
     qemu: uncaught target signal 11 (Segmentation fault) - core dumped 段错误 (核心已转储)
    

结果,必现段错误。

分析

core文件分析

正常情况下(除非有特别的设置),段错误后都会生成core文件,而coredumpctl命令为core文件的查找和分析提供了便利。通常的分析步骤如下:

  1. 执行coredumpctl命令可以列举出环境中的所有core文件的信息,比如生成时间,pid,可执行文件等,示例如下: # coredumpctl TIME PID UID GID SIG PRESENT EXE 一 2017-07-10 16:39:30 CST 6457 0 0 11 * /home/jb/qemu-2.7.0/i386-linux-user/qemu-i386 一 2017-07-10 16:39:38 CST 6461 0 0 11 * /home/jb/qemu-2.7.0/i386-linux-user/qemu-i386 一 2017-07-10 16:44:32 CST 6559 0 0 11 * /home/jb/qemu-2.7.0/i386-linux-user/qemu-i386 二 2017-07-11 15:37:51 CST 6567 0 0 11 * /home/jb/qemu-2.7.0/i386-linux-user/qemu-i386

结果是按时间降序排列的(最新生成的放在最后),vi格式,按Shift+G,就能跳转到最后,看到刚生成的core文件,此例中为

	二 2017-07-11 15:37:51 CST    6567     0     0  11 * /home/jb/qemu-2.7.0/i386-linux-user/qemu-i386

其中6745为产生core的pid。

  1. 接下来分析core文件,传统的方法需要找到core文件的位置,然后使用gdb来分析,但有coredumpctl命令之后,就不用了,直接执行

     coredumpctl gdb <pid>即可进行分析
    

示例如:

[root@localhost qemu-2.7.0]# coredumpctl gdb 6567 PID: 6567 (qemu-i386) UID: 0 (root) GID: 0 (root) Signal: 11 (SEGV) Timestamp: 二 2017-07-11 15:37:34 CST (51s ago) Command Line: i386-linux-user/qemu-i386 /media/D/jb/hello Executable: /home/jb/qemu-2.7.0/i386-linux-user/qemu-i386 Control Group: /user.slice/user-0.slice/session-80.scope Unit: session-80.scope Slice: user-0.slice Session: 80 Owner UID: 0 (root) Boot ID: 433d7c40a079410bb81063eb9eb21cb6 Machine ID: fe727eebd73b4f589ed9d8d6e071f6ff Hostname: localhost.localdomain Coredump: /var/lib/systemd/coredump/core.qemu-i386.0.433d7c40a079410bb81063eb9eb21cb6.6567.1499758654000000.xz Message: Process 6567 (qemu-i386) of user 0 dumped core.

GNU gdb (GDB) Fedora 7.8.1-31.fc21.loongson.1 Copyright (C) 2014 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later http://gnu.org/licenses/gpl.html This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type “show copying” and “show warranty” for details. This GDB was configured as “mips64el-redhat-linux-gnu”. Type “show configuration” for configuration details. For bug reporting instructions, please see: http://www.gnu.org/software/gdb/bugs/. Find the GDB manual and other documentation resources online at: http://www.gnu.org/software/gdb/documentation/. For help, type “help”. Type “apropos word” to search for commands related to “word”… Reading symbols from /home/jb/qemu-2.7.0/i386-linux-user/qemu-i386…done. [New LWP 6567] [New LWP 6568] [Thread debugging using libthread_db enabled] Using host libthread_db library “/lib64/libthread_db.so.1”. Core was generated by `i386-linux-user/qemu-i386 /media/D/jb/hello’. Program terminated with signal SIGSEGV, Segmentation fault. #0 0x000000ffeec4c0b0 in sigsuspend () from /lib64/libc.so.6 Missing separate debuginfos, use: debuginfo-install glib2-2.42.1-1.2.fc21.loongson.mips64el glibc-2.20-6.fc21.loongson.1.mips64el libgcc-4.9.2-2.fc21.loongson.1.mips64el libstdc++-4.9.2-2.fc21.loongson.1.mips64el zlib-1.2.8-8.fc21.loongson.1.mips64el (gdb)

  1. 看看堆栈

(gdb) bt #0 0x000000ffeec4c0b0 in sigsuspend () from /lib64/libc.so.6 #1 0x0000000060078b08 in force_sig (target_sig=target_sig@entry=11) at /home/jb/qemu-2.7.0/linux-user/signal.c:564 #2 0x0000000060079850 in handle_pending_signal (cpu_env=cpu_env@entry=0x62219e00, sig=sig@entry=11, k=k@entry=0x62223b28) at /home/jb/qemu-2.7.0/linux-user/signal.c:5865 #3 0x000000006007b408 in process_pending_signals (cpu_env=0x62219e00) at /home/jb/qemu-2.7.0/linux-user/signal.c:5940 #4 0x0000000060058cb8 in cpu_loop (env=0x62219e00) at /home/jb/qemu-2.7.0/linux-user/main.c:449 #5 0x000000006002476c in main (argc=, argv=0xffffe28e88, envp=) at /home/jb/qemu-2.7.0/linux-user/main.c:4837

看看这个堆栈,几乎没用,意思很简单,就是qemu在cpu_loop函数(主循环)执行过程中,收到了11信号(段错误)。

cpu_loop函数是qemu执行翻译后指令的主循环,所有翻译后的指令都在这里执行,意味着异常可能发生在任何地方。这个堆栈信息唯一能告诉我们的就只有:故障发生在翻译后的代码中,不在qemu自身的逻辑流程中。

单凭这个core和堆栈,无法继续分析了,那么,对于这种问题,应该如何分析呢?

指令级跟踪

其实,使用gdb还能进一步跟踪。

由于堆栈只能反应到函数层面的调用链,其粒度比较粗。由于cpu_loop中执行的是翻译后的二进制指令,如果要定位到具体出错的二进制指令,有两种方法:

gdb运行qemu-i386

总体思路是:使用gdb重新运行qemu-i386,直接run,然后会出现段错误,段错误信息中会有出错代码具体位置,然后使用disass反汇编查看即可。

# gdb i386-linux-user/qemu-i386 GNU gdb (GDB) Fedora 7.8.1-31.fc21.loongson.1 Copyright (C) 2014 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later http://gnu.org/licenses/gpl.html This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type “show copying” and “show warranty” for details. This GDB was configured as “mips64el-redhat-linux-gnu”. Type “show configuration” for configuration details. For bug reporting instructions, please see: http://www.gnu.org/software/gdb/bugs/. Find the GDB manual and other documentation resources online at: http://www.gnu.org/software/gdb/documentation/. For help, type “help”. Type “apropos word” to search for commands related to “word”… Reading symbols from i386-linux-user/qemu-i386…done. (gdb) set args /media/D/jb/hello (gdb) r Starting program: /home/jb/qemu-2.7.0/i386-linux-user/qemu-i386 /media/D/jb/hello Missing separate debuginfos, use: debuginfo-install glibc-2.20-6.fc21.loongson.1.mips64el [Thread debugging using libthread_db enabled] Using host libthread_db library “/lib64/libthread_db.so.1”. [New Thread 0xfff78b3190 (LWP 6665)]

Program received signal SIGSEGV, Segmentation fault. 0x00000000601f3be8 in static_code_gen_buffer () Missing separate debuginfos, use: debuginfo-install glib2-2.42.1-1.2.fc21.loongson.mips64el libgcc-4.9.2-2.fc21.loongson.1.mips64el libstdc++-4.9.2-2.fc21.loongson.1.mips64el zlib-1.2.8-8.fc21.loongson.1.mips64el (gdb) disassemble 0x00000000601f3be8 … 0x00000000601f3bc0 <+112>: li s1,0 0x00000000601f3bc4 <+116>: sw s1,20(s0) 0x00000000601f3bc8 <+120>: lw s1,16(s0) 0x00000000601f3bd0 <+128>: li a0,254 0x00000000601f3bd4 <+132>: dsll a0,a0,0x10 0x00000000601f3bd8 <+136>: ori a0,a0,0xfea0 0x00000000601f3bdc <+140>: dsll a0,a0,0x10 0x00000000601f3be0 <+144>: ori a0,a0,0x4000 0x00000000601f3be4 <+148>: daddu a0,a0,a0 => 0x00000000601f3be8 <+152>: lw s2,0(a0) 0x00000000601f3bec <+156>: addiu s1,s1,4 0x00000000601f3bf0 <+160>: sw s2,24(s0) 0x00000000601f3bf4 <+164>: move s3,s1 0x00000000601f3bf8 <+168>: sw s3,4(s0) 0x00000000601f3bfc <+172>: li s4,-16 0x00000000601f3c00 <+176>: and s1,s1,s4

可以看出,出错位置在0x00000000601f3be8,可以这样看到出错前后的汇编代码,注意,这是已经翻译后的Mips汇编代码。

gdb指令级调试

第二种方法,即使用gdb提供的指令级调试功能。这个功能不常用,但在调试类似qemu这种二进制相关的程序时,非常有用。

我们知道,gdb中单步跟踪,使用两个命令:n(单步,不进入函数)和s(单步,进入函数),这都是代码行级别的,即每步1行,注意:并非1条指令,如果要进行指令级单步,则需要使用如下命令:

	ni  //执行一条指令,不进入函数调用(call调用)
	si  //执行一条指令,进入函数调用(call调用)

对于这个问题,我们只需要,使用ni/si进行指令级单步,最终肯定会跟踪到出错的指令,而且能看到整个指令执行流。但即使最简单的程序,也会涉及太多的指令,最好分析代码,在特定的地方先断点,断住后再进行单步,这样会节省很多时间。

具体分析方法,就不列举示例了,感兴趣可以自己试试。 注意:指令级单步时,需要结合layout asm命令使用,以便于查看汇编代码,具体可以看看相关命令的手册。

what’s next?

对于这个问题来说,通过上述放到找到具体出错的二进制代码了,但是光看这些个汇编代码,能说明什么问题?看似还是毫无用处嘛,根本不知道它在干嘛。

咋看如此,但其实如果对qemu原理和Mips指令熟悉的同学来说,也能直接看懂,或许能看出问题所在。但是,对于普通人来说,显然不现实。那接下来该怎么办呢?

其实,qemu自身就提供了相应的调试机制,可以用于调式qemu中运行的目标代码,同时,还可以将qemu的代码翻译过程全部打印出来,包括:原始的输入汇编代码(x86代码)、qemu生成的中间代码、qemu翻译后的目标代码(Mips代码),关于qemu的相关机制,后续有时间再单独写文章说明了,这里就不发散了。

qemu自带的调式

远程交叉调式

qemu自带了调试机制,本质上,其自身实现了一个gdbserver(模仿gdb的代码),可以通过远程交叉调试。

如果需要启用相关调式,需要使用如下参数:

	-g <端口号>

比如,以如下方式启动qemu:

	# i386-linux-user/qemu-i386 -g 1234 /media/D/jb/hello

那么,在远程主机(另一台主机,或者本机另一个终端)上通过gdb remote方式即可连接上qemu自带的gdbserver,然后进行基本的调式操作,示例如:

[root@localhost qemu-2.7.0]# gdb GNU gdb (GDB) Fedora 7.8.1-31.fc21.loongson.1 Copyright (C) 2014 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later http://gnu.org/licenses/gpl.html This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type “show copying” and “show warranty” for details. This GDB was configured as “mips64el-redhat-linux-gnu”. Type “show configuration” for configuration details. For bug reporting instructions, please see: http://www.gnu.org/software/gdb/bugs/. Find the GDB manual and other documentation resources online at: http://www.gnu.org/software/gdb/documentation/. For help, type “help”. Type “apropos word” to search for commands related to “word”. (gdb) target remote :1234 Remote debugging using :1234 0x00000000 in ?? ()

当然,这样调试需要对qemu和被调式代码有深入的理解,才可能操作。上面的堆栈信息有问题,是因为hello程序是x86程序,其依赖的x86环境中的基础库(如C库)在龙芯环境中没有,所以,如果要完整进行分析调试,还需要在龙芯环境中搭建一个chroot的x86环境,在其中运行x86程序,这又是一个复杂的话题了,后续单独写文章说明。

这里,只需要了解,qemu提供了这样的调试方式,并且很有用。

跟踪qemu指令翻译过程

-g参数能让qemu支持远程调式,如果要打印qemu的指令翻译过程,则需要在-g的基础上加上如下参数组合,这个参数组合也是qemu调式中非常有用的,使用频率很高:

	-d in_asm,op,out_asm
  • in_asm, 表示输入的汇编指令,本例中即为i386汇编指令
  • op,表示,qemu翻译后生成的中间指令。qemu会将所有的输入指令翻译成与硬件无关的中间指令,然后再将中间指令翻译成目标指令。
  • out_asm。表示qemu翻译后的目标指令,本例中即为Mips64指令。

加上此参数组合中,所有的指令翻译过程都会打印出来。示例如:

----------------
IN: 
0x54b612d2:  jmp    *0x54b62130

OP:
 ld_i32 tmp11,env,$0xfffffffffffffff0
 movi_i32 tmp12,$0x0
 brcond_i32 tmp11,tmp12,ne,$L0

 ---- 54b612d2 00000000
 movi_i32 tmp2,$0x54b62130
 qemu_ld_i32 tmp0,tmp2,leul,0
 st_i32 tmp0,env,$0x20
 call debug,$0x0,$0,env
 set_label $L0
 exit_tb $0xfff5d10013

OUT: [size=100]
0x60393aa4:  lw	s1,-16(s0)
0x60393aa8:  bne	zero,s1,0x60393af0
0x60393aac:  nop
0x60393ab0:  lui	s1,0x54b6
0x60393ab4:  ori	s1,s1,0x2130
0x60393ab8:  dext	a0,s1,0x0,0x20
0x60393abc:  move	s1,a0
0x60393ac0:  li	a0,254
0x60393ac4:  dsll	a0,a0,0x10
0x60393ac8:  ori	a0,a0,0xfed1
0x60393acc:  dsll	a0,a0,0x10
0x60393ad0:  daddu	a0,a0,s1
0x60393ad4:  lw	s1,0(a0)
0x60393ad8:  sw	s1,32(s0)
0x60393adc:  move	a0,s0
0x60393ae0:  lui	t9,0x600a
0x60393ae4:  ori	t9,t9,0x66b0
0x60393ae8:  jal	0x600a66b0
0x60393aec:  nop
0x60393af0:  li	v0,255
0x60393af4:  dsll	v0,v0,0x10
0x60393af8:  ori	v0,v0,0xf5d1
0x60393afc:  dsll	v0,v0,0x10
0x60393b00:  j	0x60393a74
0x60393b04:  ori	v0,v0,0x13

其中,可以清楚的看到翻译过程,每条输入指令都会被翻译成数条中间指令,然后每条中间指令会再次被翻译成数条目标指令,整体看估计有一个数量级的性能损耗。

对照X86程序代码

看到这里,TX可能还是很疑问,这个指令翻译好像也忒难看懂了,也看不出问题呀,接下来能怎么办?

接下来,需要对照X86程序的汇编代码,可以找到出错的X86程序对应的代码行,具体可以使用objdump工具,示例如(要在X86环境中执行哈):

# objdump -dl /home/hello|less >... >08048a00 <main>: >main(): >/home/hello.c:3 > 8048a00:       8d 4c 24 04             lea    0x4(%esp),%ecx > 8048a04:       83 e4 f0                and    $0xfffffff0,%esp > 8048a07:       ff 71 fc                pushl  -0x4(%ecx) > 8048a0a:       55                      push   %ebp > 8048a0b:       89 e5                   mov    %esp,%ebp > 8048a0d:       51                      push   %ecx > 8048a0e:       83 ec 04                sub    $0x4,%esp >/home/hello.c:4 > 8048a11:       83 ec 0c                sub    $0xc,%esp > 8048a14:       68 0c 32 0c 08          push   $0x80c320c > 8048a19:       e8 c2 0a 00 00          call   80494e0 <_IO_puts> > 8048a1e:       83 c4 10                add    $0x10,%esp >/home/hello.c:5 > 8048a21:       8b 4d fc                mov    -0x4(%ebp),%ecx > 8048a24:       c9                      leave   > 8048a25:       8d 61 fc                lea    -0x4(%ecx),%esp > 8048a28:       c3                      ret     > 8048a29:       66 90                   xchg   %ax,%ax > 8048a2b:       66 90                   xchg   %ax,%ax > 8048a2d:       66 90                   xchg   %ax,%ax

如此,可以根据上述qemu编译过程中的输入汇编的地址,对照找到对应的代码行。

问题如何产生?

讲了这么多,好像还是看不懂,看不出问题?

确实很难看懂,但所有调试信息都在这里了,接下来。。。

先找到出错地址(目标环境中的地址)附近的代码:

0x00000000601f3bc0 <+112>: li s1,0 0x00000000601f3bc4 <+116>: sw s1,20(s0) 0x00000000601f3bc8 <+120>: lw s1,16(s0) 0x00000000601f3bd0 <+128>: li a0,254 0x00000000601f3bd4 <+132>: dsll a0,a0,0x10 0x00000000601f3bd8 <+136>: ori a0,a0,0xfea0 0x00000000601f3bdc <+140>: dsll a0,a0,0x10 0x00000000601f3be0 <+144>: ori a0,a0,0x4000 0x00000000601f3be4 <+148>: daddu a0,a0,a0 => 0x00000000601f3be8 <+152>: lw s2,0(a0) 0x00000000601f3bec <+156>: addiu s1,s1,4 0x00000000601f3bf0 <+160>: sw s2,24(s0) 0x00000000601f3bf4 <+164>: move s3,s1 0x00000000601f3bf8 <+168>: sw s3,4(s0) 0x00000000601f3bfc <+172>: li s4,-16 0x00000000601f3c00 <+176>: and s1,s1,s4

出错指令:

lw	s2,0(a0)

显然,是a0寄存器出问题了,再往前看,看看a0寄存器的来世今生,可以看出前面的逻辑有点问题:

0x00000000601f3be0 <+144>: ori a0,a0,0x4000 //这里将a0与立即数进行or操作 0x00000000601f3be4 <+148>: daddu a0,a0,a0 //这里相当于将a0内容乘以2,这里显然有问题

因为,a0在出错的指令中是用作寻址用的,而这里确做了乘2的操作,地址乘2显然是不可能的。

显然,这里翻译后的代码有问题,那需要继续确认对应的中间指令,确认中间指令是否正常。根据gdb -d in_asm,op,out_asm的调试结果,可以找到该段目标指令对应的中间指令,对应的中间指令为:

qemu_ld_i32

该指令从名称上看就基本明白用途了:load from i,即加载32位的立即数到指定内存地址中。

再分析一下,该中间指令的上下文与其对应的输入指令,确认中间指令的翻译是否正常,这里就不具体列代码了,需要靠理解。本例中,可以确认中间指令是没有问题的,所以,疑点就在于中间指令到目标指令的翻译过程中,即对qemu_ld_i32的翻译有问题。

接下来该怎么办?

当然是看代码了,这是解决所有问题的根本。

找到翻译qemu_ld_i32指令的相关代码(这个需要分析qemu的基本原理和对qemu代码的理解,不容易),对应的代码位于tcg/mips/tcg-target.inc.c文件中,该文件负责所有中间指令到Mips指令的翻译,对应函数为:tcg_out_qemu_ld,看看具体实现:

static void tcg_out_qemu_ld(TCGContext *s, const TCGArg *args, bool is_64)
{
	/*
	 * 定义临时变量,用于从入参中解析参数。
	 * addr_regl, load目标地址的低半部分.
	 * addr_regh, load目标地址的高半部分,64位时使用.
	 * data_regl, 被加载数据(此例为立即数)的低半部分.
	 * data_regh, 被加载数据(此例为立即数)的高半部分,64位时使用.
	 * opc, 指令操作符
	 */
    TCGReg addr_regl, addr_regh __attribute__((unused));
    TCGReg data_regl, data_regh;
    TCGMemOpIdx oi;
    TCGMemOp opc;
#if defined(CONFIG_SOFTMMU)
    tcg_insn_unit *label_ptr[2];
#endif
    // 用于存放地址的中间临时的寄存器,此处使用A0寄存器。
    TCGReg base = TCG_REG_A0;
    // 从入参中解析参数,放入特定的变量
    data_regl = *args++;
    data_regh = (TCG_TARGET_REG_BITS == 32 && is_64 ? *args++ : 0);
    addr_regl = *args++;
    addr_regh = (TCG_TARGET_REG_BITS < TARGET_LONG_BITS ? *args++ : 0);
    oi = *args++;
    opc = get_memop(oi);

#if defined(CONFIG_SOFTMMU)
    tcg_out_tlb_load(s, base, addr_regl, addr_regh, oi, label_ptr, 1);
    tcg_out_qemu_ld_direct(s, data_regl, data_regh, base, opc, is_64);
    add_qemu_ldst_label(s, 1, oi,
                        (is_64 ? TCG_TYPE_I64 : TCG_TYPE_I32),
                        data_regl, data_regh, addr_regl, addr_regh,
                        s->code_ptr, label_ptr);
#else
     /*
       * 主机的寄存器宽度大于long型宽度,如此例中在64位的Mips CPU环境中运行
       * 32位的x86程序就是这种情况
       */
    if (TCG_TARGET_REG_BITS > TARGET_LONG_BITS) {
         /*
           * 将地址addr_regl(32位,由于是32位程序,所以只用了addr_regl,
           * 不使用addr_regh) zero-extended至64位。
           */
        tcg_out_ext32u(s, base, addr_regl);
          /*
            * 将addr_regl设置为A0寄存器,即addr_regl也要使用A0寄存器了,
            * 这里很关键,需要理解。
            */
        addr_regl = base;
    }
    if (guest_base == 0 && data_regl != addr_regl) {
        base = addr_regl;
    } else if (guest_base == (int16_t)guest_base) {
        tcg_out_opc_imm(s, ALIAS_PADDI, base, addr_regl, guest_base);
    } else {
        /* 
          * 这段代码的目的是,将guest_base与addr_regl相加。为什么这么做?
       * guest_base可理解为基地址,这里guest的意思是目标程序作为主机的
       * guest,类似虚拟机。因为整个guest的地址空间都是qemu虚拟出来的,
         * 本质上就是在qemu进程自己的地址空间中分配了一段内存(虚拟地址)用
         * 作guest专用,这里的guest_base就是这段内存的起始地址,guest
         * (虚拟机)中分配的内存都以此作偏移,那么guest_base+addr就作为虚
         * 拟机中目标程序的寻址方式了。所以,这里的操作相当于是对目标程序的
         * 内存地址的寻址。比较绕,好好理解下!
         */
         /*
           * 这里先把guest_base赋值给base(即A0寄存器),注意:此时A0寄存器
           * 中的内容已经变成guest_base了
           */
        tcg_out_movi(s, TCG_TYPE_PTR, base, guest_base);
        /*
          * 这里又将addr_regl和base相加,然后放入base中。注意:这里的
       * addr_regl已经是A0了(前面赋值),而base也是A0,那结果就相当于
       * A0+A0-->A0,相当于就是把A0乘2,再放回A0,这个逻辑就跟出问题
          * 时的目标代码一模一样了,的确是这里产生了错误的目标代码。
          */
        tcg_out_opc_reg(s, ALIAS_PADD, base, base, addr_regl);
    }
    tcg_out_qemu_ld_direct(s, data_regl, data_regh, base, opc, is_64);
#endif
}

说实话,这段代码很难懂,需要深入理解qemu翻译代码的原理,并且熟悉Mips汇编才行。但结合上面的注释,应该比较容易看懂了。

我们再来理解下,为何会产生这样的问题?显然作者这样写代码是有目的的。

问题其实就出在临时寄存器上,由于不能直接修改输入操作数addr_regl(为什么不能?自己理解),所以对其进行扩展操作需要使用临时寄存器,此处,作者选择了A0(即base变量),而另一方面,进行寻址操作时,需要对guest_base进行算术操作,其操作结果也需要一个临时寄存器来保存,这里作者仍旧选择了A0,于是产生了冲突两个都用A0,导致A0中的内容冲突,导致了这样的结果。

解决

根据上述的分析,其实解决方案已经相当明显了,临时寄存器冲突,解决思路:只需要让两个操作使用不同的临时寄存器即可。addr_regl扩展时使用了A0,那么寻址时使用另一个临时寄存器即可。tcg Mips中代码中,默认就定义了几个专门的临时寄存器:

#define TCG_TMP0  TCG_REG_AT
#define TCG_TMP1  TCG_REG_T9
#define TCG_TMP2  TCG_REG_T8
#define TCG_TMP3  TCG_REG_T7

即AT,T9,T8,T7这四个寄存器可用做临时寄存器。所以,最简单的解决方法,就是让寻址时使用TCG_TMP0之类的临时寄存器。

对应补丁为(已合入开源):

 tcg/mips/tcg-target.inc.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/tcg/mips/tcg-target.inc.c b/tcg/mips/tcg-target.inc.c
index 8cff9a6..85756b8 100644
--- a/tcg/mips/tcg-target.inc.c
+++ b/tcg/mips/tcg-target.inc.c
@@ -1547,8 +1547,8 @@ static void tcg_out_qemu_ld(TCGContext *s, const TCGArg *args, bool is_64)
     } else if (guest_base == (int16_t)guest_base) {
         tcg_out_opc_imm(s, ALIAS_PADDI, base, addr_regl, guest_base);
     } else {
-        tcg_out_movi(s, TCG_TYPE_PTR, base, guest_base);
-        tcg_out_opc_reg(s, ALIAS_PADD, base, base, addr_regl);
+        tcg_out_movi(s, TCG_TYPE_PTR, TCG_TMP0, guest_base);
+        tcg_out_opc_reg(s, ALIAS_PADD, base, TCG_TMP0, addr_regl);
     }
     tcg_out_qemu_ld_direct(s, data_regl, data_regh, base, opc, is_64);
 #endif
@@ -1652,8 +1652,8 @@ static void tcg_out_qemu_st(TCGContext *s, const TCGArg *args, bool is_64)
     } else if (guest_base == (int16_t)guest_base) {
         tcg_out_opc_imm(s, ALIAS_PADDI, base, addr_regl, guest_base);
     } else {
-        tcg_out_movi(s, TCG_TYPE_PTR, base, guest_base);
-        tcg_out_opc_reg(s, ALIAS_PADD, base, base, addr_regl);
+        tcg_out_movi(s, TCG_TYPE_PTR, TCG_TMP0, guest_base);
+        tcg_out_opc_reg(s, ALIAS_PADD, base, TCG_TMP0, addr_regl);
     }
     tcg_out_qemu_st_direct(s, data_regl, data_regh, base, opc);
 #endif

但是其实有更好的解决方法(qemu该模块的maintainer建议的),就是为guest_base保留单独的寄存器专用,因为guest_base的使用频率非常高,保留寄存器就不用每次都重新从内存中提取值到寄存器中,可以提升效率。目前ppc/sparc/ia64等架构代码都是这么实现的,于是参考ppc代码提交了新补丁,保留S1寄存器做guest_base用,补丁如下(已合入开源):

 tcg/mips/tcg-target.inc.c | 17 +++++++++++++----
 1 file changed, 13 insertions(+), 4 deletions(-)

diff --git a/tcg/mips/tcg-target.inc.c b/tcg/mips/tcg-target.inc.c
index 85756b8..1a8169f 100644
--- a/tcg/mips/tcg-target.inc.c
+++ b/tcg/mips/tcg-target.inc.c
@@ -85,6 +85,10 @@ static const char * const tcg_target_reg_names[TCG_TARGET_NB_REGS] = {
 #define TCG_TMP2  TCG_REG_T8
 #define TCG_TMP3  TCG_REG_T7
 
+#ifndef CONFIG_SOFTMMU
+#define TCG_GUEST_BASE_REG TCG_REG_S1
+#endif
+
 /* check if we really need so many registers :P */
 static const int tcg_target_reg_alloc_order[] = {
     /* Call saved registers.  */
@@ -1547,8 +1551,7 @@ static void tcg_out_qemu_ld(TCGContext *s, const TCGArg *args, bool is_64)
     } else if (guest_base == (int16_t)guest_base) {
         tcg_out_opc_imm(s, ALIAS_PADDI, base, addr_regl, guest_base);
     } else {
-        tcg_out_movi(s, TCG_TYPE_PTR, TCG_TMP0, guest_base);
-        tcg_out_opc_reg(s, ALIAS_PADD, base, TCG_TMP0, addr_regl);
+        tcg_out_opc_reg(s, ALIAS_PADD, base, TCG_GUEST_BASE_REG, addr_regl);
     }
     tcg_out_qemu_ld_direct(s, data_regl, data_regh, base, opc, is_64);
 #endif
@@ -1652,8 +1655,7 @@ static void tcg_out_qemu_st(TCGContext *s, const TCGArg *args, bool is_64)
     } else if (guest_base == (int16_t)guest_base) {
         tcg_out_opc_imm(s, ALIAS_PADDI, base, addr_regl, guest_base);
     } else {
-        tcg_out_movi(s, TCG_TYPE_PTR, TCG_TMP0, guest_base);
-        tcg_out_opc_reg(s, ALIAS_PADD, base, TCG_TMP0, addr_regl);
+        tcg_out_opc_reg(s, ALIAS_PADD, base, TCG_GUEST_BASE_REG, addr_regl);
     }
     tcg_out_qemu_st_direct(s, data_regl, data_regh, base, opc);
 #endif
@@ -2452,6 +2454,13 @@ static void tcg_target_qemu_prologue(TCGContext *s)
                    TCG_REG_SP, SAVE_OFS + i * REG_SIZE);
     }
 
+#ifndef CONFIG_SOFTMMU
+    if (guest_base) {
+        tcg_out_movi(s, TCG_TYPE_PTR, TCG_GUEST_BASE_REG, guest_base);
+        tcg_regset_set_reg(s->reserved_regs, TCG_GUEST_BASE_REG);
+    }
+#endif
+
     /* Call generated code */
     tcg_out_opc_reg(s, OPC_JR, 0, tcg_target_call_iarg_regs[1], 0);
     /* delay slot *

具体内容自己理解下。