by xmj

1、gcc的configure支持–with-abi和–with-arch等选项:

--with-schedule=cpu
--with-arch=cpu
--with-arch-32=cpu
--with-arch-64=cpu
--with-tune=cpu
--with-tune-32=cpu
--with-tune-64=cpu
--with-abi=abi
--with-fpu=type
--with-float=type
   These configure options provide default values for the -mschedule=, -march=,
-mtune=, -mabi=, and -mfpu= options and for -mhard-float or -msoft-float. As with
--with-cpu, which switches will be accepted and acceptable values of the arguments
depend on the target.

2、在configure的时候,如果指定了这些选项,那么gcc/config.gcc则会将这些值保存在configure_default_options变量中:

#  configure_default_options
#                       Set to an initializer for configure_default_options
#                       in configargs.h, based on --with-cpu et cetera.

这个变量值会被写入build目录下的configargs.h文件中,形如:

static const struct {
 const char *name, *value;
} configure_default_options[] = { { "abi", "o32" }, { "arch", "mips1" } };

3、gcc提供了一个目标宏,供port来定义缺省选项的spec

— Macro: OPTION_DEFAULT_SPECS

   A list of specs used to support configure-time default options
(i.e. --with options) in the driver. It should be a suitable
initializer for an array of structures, each containing two strings,
without the outermost pair of surrounding braces.

   The first item in the pair is the name of the default. This must
match the code in config.gcc for the target. The second item is a spec
to apply if a default with this name was specified. The string
`%(VALUE)' in the spec will be replaced by the value of the default
everywhere it occurs.

   The driver will apply these specs to its own command line between
loading default specs files and processing DRIVER_SELF_SPECS, using
the same mechanism as DRIVER_SELF_SPECS.

   Do not define this macro if it does not need to do anything.

4、gcc启动时,会分析option_default_specs和configure_default_options,将configure_default_options中的值替换到相应的option_default_specs中:

 /* Process any configure-time defaults specified for the command line
    options, via OPTION_DEFAULT_SPECS.  */
 for (i = 0; i < ARRAY_SIZE (option_default_specs); i++)
   do_option_spec (option_default_specs[i].name,
                   option_default_specs[i].spec);

这样,gcc就会在传给cc1和as等程序的选项中,增加了这些在configure时指定的选项。

 

by xmj, yao

一、x86 ABI手册原文及翻译

原文摘自SYSTEM V APPLICATION BINARY INTERFACE。

Figure 5-7: Position-Independent Procedure Linkage Table

.PLT0: pushl 4(%ebx)
       jmp *8(%ebx)
       nop; nop
       nop; nop
.PLT1: jmp *name1@GOT(%ebx)
       pushl $offset
       jmp .PLT0@PC
.PLT2: jmp *name2@GOT(%ebx)
       pushl $offset
       jmp .PLT0@PC
...

Following the steps below, the dynamic linker and the program ‘‘cooperate’’ to resolve symbolic references through the procedure linkage table and the global offset table.

动态链接器和程序,按照下面的步骤,协作完成对通过过程链接表和全局偏移表进行符号引用的解析。

1 . When first creating the memory image of the program, the dynamic linker sets the second and the third entries in the global offset table to special values. Steps below explain more about these values.

动态链接器在开始创建程序的内存映像时,会将全局偏移表中的第二,三项设置为特定的值。这些值在下面的步骤中详细解释。

2 . If the procedure linkage table is position-independent, the address of the global offset table must reside in %ebx. Each shared object file in the process image has its own procedure linkage table, and control transfers to a procedure linkage table entry only from within the same object file. Consequently, the calling function is responsible for setting the global offset table base register before calling the procedure linkage table entry.

如果过程链接表是位置无关的,则全局偏移表的地址必须存在%ebx中。进程映像中的每个共享目标文件都有自己的过程链接表,并且只能从同一个目标文件中才能将控制转换到过程链接表的表项。因此,调用函数需要在调用过程链接表项之前,设置全局偏移表的基础寄存器。

3 . For illustration, assume the program calls name1, which transfers control to the label .PLT1.

例如,假设程序调用了name1,其将控制转换到标号.PLT1.

4 . The first instruction jumps to the address in the global offset table entry for name1. Initially, the global offset table holds the address of the following
pushl instruction, not the real address of name1.

第一条指令跳转到全局偏移表项中name1的地址。初始的时候,全局偏移表中存放的是pushl指令之后的地址,而不是name1的实际地址。

5 . Consequently, the program pushes a relocation offset (offset) on the stack. The relocation offset is a 32-bit, non-negative byte offset into the relocation table. The designated relocation entry will have type R_386_JMP_SLOT, and its offset will specify the global offset table entry used in the previous jmp instruction. The relocation entry also contains a symbol table index, thus telling the dynamic linker what symbol is being referenced, name1 in this case.

因此,程序将一个重定位偏移量(offset)压入栈中。重定位偏移量为一个32位,非负的,重定位表的字节偏移。其所指定的重定位项将具有R_386_JMP_SLOT类型,并且它的偏移量指定了在之前jmp指令中会用到的全局偏移表项。重定位项还包含了一个符号表索引,因此告诉了动态链接器哪个符号在被引用。在该例子中,为name1.

6 . After pushing the relocation offset, the program then jumps to .PLT0, the first entry in the procedure linkage table. The pushl instruction places the value of the second global offset table entry (got_plus_4 or 4(%ebx)) on the stack, thus giving the dynamic linker one word of identifying information. The program then jumps to the address in the third global offset table entry (got_plus_8 or 8(%ebx)), which transfers control to the dynamic linker.

在压入重定位偏移量之后,程序然后跳转到.PLT0,过程链接表的第一项。pushl指令将全局偏移表的第二个表项(got_plus_4 or 4(%ebx))压入栈中,因此给了动态链接器一个字的标识信息。程序然后跳转到全局偏移表的第三个表项中(got_plus_8 or 8(%ebx))的地址,其将控制转换给动态链接器。

7 . When the dynamic linker receives control, it unwinds the stack, looks at the designated relocation entry, finds the symbol’s value, stores the ‘‘real’’ address for name1 in its global offset table entry, and transfers control to the desired destination.

当动态链接器获得控制之后,其展开栈,查看指定的重定位项,发现符号的值,将name1的“实际”地址存放在它的全局偏移表项中,然后将控制转换到所希望的目的地。

8 . Subsequent executions of the procedure linkage table entry will transfer directly to name1, without calling the dynamic linker a second time. That is, the jmp instruction at .PLT1 will transfer to name1, instead of ‘‘falling through’’ to the pushl instruction.

以后对过程链接表项的执行,将会直接转换到name1,而不需要再次调用动态链接器。也就是说,在.PLT1中的jmp指令会直接跳转到name1,而不会顺序执行到pushl指令。

二、实例分析

1、为了帮助理解这些枯燥的文档,我们结合一个实际的例子进行分析。

例子很简单,

#include 

int
main (void)
{
 printf ("hellogcc\n");

 return 0;
}

2、后边我们会看到一些汇编程序和一些地址,为了搞清楚这些地址的含义,我们先列出一些段的地址范围,

(gdb) maintenance info sections
Exec file:
   `/home/yao/SourceCode/plt.exe', file type elf32-i386.
   0x80481d4->0x8048224 at 0x000001d4: .dynsym ALLOC LOAD READONLY DATA HAS_CONTENTS
   0x8048224->0x804826e at 0x00000224: .dynstr ALLOC LOAD READONLY DATA HAS_CONTENTS
   0x8048298->0x80482a0 at 0x00000298: .rel.dyn ALLOC LOAD READONLY DATA HAS_CONTENTS
   0x80482a0->0x80482b8 at 0x000002a0: .rel.plt ALLOC LOAD READONLY DATA HAS_CONTENTS
   0x80482b8->0x80482e8 at 0x000002b8: .init ALLOC LOAD READONLY CODE HAS_CONTENTS
   0x80482e8->0x8048328 at 0x000002e8: .plt ALLOC LOAD READONLY CODE HAS_CONTENTS
   0x8048330->0x804849c at 0x00000330: .text ALLOC LOAD READONLY CODE HAS_CONTENTS
   0x804849c->0x80484b8 at 0x0000049c: .fini ALLOC LOAD READONLY CODE HAS_CONTENTS
   0x80484b8->0x80484c9 at 0x000004b8: .rodata ALLOC LOAD READONLY DATA HAS_CONTENTS
   0x80484cc->0x80484d0 at 0x000004cc: .eh_frame ALLOC LOAD READONLY DATA HAS_CONTENTS
   0x8049f0c->0x8049f14 at 0x00000f0c: .ctors ALLOC LOAD DATA HAS_CONTENTS
   0x8049f14->0x8049f1c at 0x00000f14: .dtors ALLOC LOAD DATA HAS_CONTENTS
   0x8049f1c->0x8049f20 at 0x00000f1c: .jcr ALLOC LOAD DATA HAS_CONTENTS
   0x8049f20->0x8049ff0 at 0x00000f20: .dynamic ALLOC LOAD DATA HAS_CONTENTS
   0x8049ff0->0x8049ff4 at 0x00000ff0: .got ALLOC LOAD DATA HAS_CONTENTS
   0x8049ff4->0x804a00c at 0x00000ff4: .got.plt ALLOC LOAD DATA HAS_CONTENTS
   0x804a00c->0x804a014 at 0x0000100c: .data ALLOC LOAD DATA HAS_CONTENTS
   0x804a014->0x804a01c at 0x00001014: .bss ALLOC

3、我们看看实际程序中,我们PLT section里边的内容是什么?

(gdb) disassemble 0x80482e8,0x8048328
Dump of assembler code from 0x80482e8 to 0x8048328:
  0x080482e8:  pushl  0x8049ff8
  0x080482ee:  jmp    *0x8049ffc
  0x080482f4:  add    %al,(%eax)
  0x080482f6:  add    %al,(%eax)
  0x080482f8:  jmp    *0x804a000
  0x080482fe:  push   $0x0
  0x08048303:  jmp    0x80482e8
  0x08048308:  jmp    *0x804a004
  0x0804830e:  push   $0x8
  0x08048313:  jmp    0x80482e8
  0x08048318:  jmp    *0x804a008
  0x0804831e:  push   $0x10
  0x08048323:  jmp    0x80482e8

我们看到了,puts的plt entry,是 plt 3,前边的0 1 和 2都已经被占用了。这些都是系统
保留的entry。不同的体系结构,这里可能占用不同的书目的entry。plt 0会在本文中介绍
到,但是 plt 1 和 2 的作用,没有在本文介绍。

4、第一条指令跳转到全局偏移表项中name1的地址。初始的时候,全局偏移表中存放的是pushl指令之后的地址,而不是name1的实际地址。

  0x08048318:     jmp    *0x804a008 // -> jmp *(_GLOBAL_OFFSET_TABLE_+20)
  0x0804831e:     push   $0x10      // push relocation offset.

我们可以看到 0x804a008 落在的 .got.plt 的范围,

   0x8049ff4->0x804a00c at 0x00000ff4: .got.plt ALLOC LOAD DATA HAS_CONTENTS

(gdb) x/4x 0x804a008
0x804a008 :   0x0804831e      0x00000000      0x00000000      0x00000000

5 . 因此,程序将一个重定位偏移量(offset)压入栈中 (see the insn on 0x0804831e: push 0×10)。重定位偏移量为一个32位,非负的,重定位表的字节偏移。其所指定的重定位项将具有R_386_JMP_SLOT类型,并且它的偏移量指定了在之前jmp指令中会用到的全局偏移表项。

Relocation section '.rel.plt' at offset 0x2a0 contains 3 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0804a000  00000107 R_386_JUMP_SLOT   00000000   __gmon_start__
0804a004  00000207 R_386_JUMP_SLOT   00000000   __libc_start_main
0804a008  00000307 R_386_JUMP_SLOT   00000000   puts

我们可以看到,这里有一个reloc R_386_JUMP_SLOT,对应的地址是0x804a008,其实就是 puts对应的 .got.plt 的entry。

重定位项还包含了一个符号表索引,因此告诉了动态链接器哪个符号在被引用。在该例子中,为name1.

这个offset(0×10)是指的.rel.plt段的偏移,也就是第三项

0804a008  00000307 R_386_JUMP_SLOT   00000000   puts

这里可以看出,.rel.plt的每一项是8个字节,我手中的这个ABI手册比较旧,没有对这个段和每一项的大小做介绍。

重定位项R_386_JUMP_SLOT包含了offset,info,type,symbol这些信息。其中offset(0x0804a008)指定了在之前jmp指令中会用到的全局偏移表项,symbol信息告诉动态链接器哪个符号在被引用。动态链接器要做的事情就是将这个符号的实际值(即name1的值)填写到偏移量为0x0804a008的全局偏移表项中,即更新name1的全局偏移表项。

6 . 在压入重定位偏移量之后,程序然后跳转到.PLT0,过程链接表的第一项。

  0x08048323
:    jmp    0x80482e8  // jump to start of .plt section.

.PLT0:
  0x080482e8:  pushl  0x8049ff8
  0x080482ee:  jmp    *0x8049ffc

pushl指令将.got.plt的第二个表项(got_plus_4 or 4(%ebx))压入栈中,因此给了动态链接器一个字的标识信息。

  0x8049ff4->0x804a00c at 0x00000ff4: .got.plt ALLOC LOAD DATA HAS_CONTENTS

程序然后跳转到.got.plt的第三个表项中(got_plus_8 or 8(%ebx))的地址,其将控制转换给动态链接器。

(gdb) x/x 0x8049ffc
0x8049ffc :    0x00123270
(gdb) disassemble 0x00123270,0x00123280
Dump of assembler code from 0x123270 to 0x123280:
  0x00123270 :  push   %eax
  0x00123271 :  push   %ecx
  0x00123272 :  push   %edx
  0x00123273 :  mov    0x10(%esp),%edx
  0x00123277 :  mov    0xc(%esp),%eax
  0x0012327b : call   0x11d5a0

可以看到,`jmp *0x8049ffc’ 跳转到了 _dl_runtime_resolve,为动态链接器的入口。

7 . 当动态链接器获得控制之后,其展开栈,查看指定的重定位项,发现符号的值,将name1的“实际”地址存放在它的全局偏移表项中,然后将控制转换到所希望的目的地。

0x804a008 :   0x0804831e

让我们看看dynamic linker如何修改这个,我们在0x804a008上设置一个硬件watchpoint

(gdb) watch *0x804a008
Hardware watchpoint 2: *0x804a008
(gdb) c
Continuing.
Hardware watchpoint 2: *0x804a008

Old value = 134513438
New value = 1616016
_dl_fixup (l=, reloc_arg=) at dl-runtime.c:155
155     dl-runtime.c: No such file or directory.
       in dl-runtime.c

我们可以看到,地址0x804a008上的内容,从134513438 变化到了1616016,

(gdb) p/x 134513438
$2 = 0x804831e
(gdb) p/x 1616016
$3 = 0x18a890

我们看看 这个新地址 (1616016 0x18a890) 是什么

(gdb) disassemble 0x18a890,0x18a8a0
Dump of assembler code from 0x18a890 to 0x18a8a0:
  0x0018a890 :     push   %ebp
  0x0018a891 :     mov    %esp,%ebp
  0x0018a893 :     sub    $0x20,%esp
  0x0018a896 :     mov    %ebx,-0xc(%ebp)
  0x0018a899 :     mov    0x8(%ebp),%eax
  0x0018a89c :    call   0x143a0f

Yay!, 我们能看到地址0x804a008上的内容已经变化成为了实际的glibc中的地址了。

(gdb) bt
#0  _dl_fixup (l=, reloc_arg=) at dl-runtime.c:155
#1  0x00123280 in _dl_runtime_resolve () at ../sysdeps/i386/dl-trampoline.S:37
#2  0x080483f9 in main () at plt.c:6
 

最近GDB社区在讨论给GDB中增加ITSET的功能,这是很重要的功能,会GDB的功能,特别是多线程/多进程方面,有一个巨大的进步 (截止到2012年5月6日,ITSET功能还没有进入到GDB的 CVS)。

ITSET是什么?

ITSET是 Inferior(在GDB中,一个被调试程序的进程可以认为是一个Inferior/Thread SET 的简称。ITSET的概念本身很简单,就是一个Inferior/Thread的集合,然后有了这样的集合以后,调试器的操作范围都局限于用户设定的ITSET中。我后边会介绍 调试器的操作范围的具体含义,其实也很容易理解。ITSET在别的场合,也有不同但是类似的名字,比如,ptset (process/thread set) 或者  ptc set (process/thread/core set)。在HPDF (High Performance Debugging Forum) spec 中,叫做ptset。TotalView 中的实现也叫做ptset。每个实现的细节语法和功能和 HPDF spec 有一些出入,但是大体上都一样的。

调试的操作范围,其实,就是一个命令对被调试程序的影响范围。假如调试器正在调试两个进程,p1 和 p2。每个进程又有三个进程,t11, t12,t13 和 t21,t22,t23。如果你是一个调试器的设计者,当实现如下命令的时候,你就需要考虑命令的操作范围了,

    break foo 假如两个进程都有foo这个符号,那么断点是插入到两个进程里边呢,还是仅仅插入到当前进程里边?也就是说,这个命令的操作范围是当前进程呢还是所有进程。当然,还可以有更加细致的控制范围,比如,现在有100个进程,我想断点只插入到若干几个进程中。
    break bar 假如只有一个进程有这个符号,理所当然这个断点就只插入到那一个进程中。当进程中的一个线程触发断点的时候,应该有哪些线程受到影响呢?这里又有一个范围的问题,需要设计者考虑。p1进程中的t11触发了线程,调试器应该让那几个线程停止下来?调试器的设计者,有如下几个选择

      只停止t11。因为就是它触发了断点,其它线程和进程继续运行
      停止p1中的所有线程。因为它的一个线程触发了断点
      停止p1和p2中的所有线程。这样虽然有些不可思议,但是的确是一种选择

    其实还有很多别的选择,这里就不罗列了。在以前的调试器设计中,往往都是选定一种控制范围。

    continue 运行这个命令,就是resume已经停止了的线程或者进程继续执行。这里也有不同的范围考虑。如果当前进程p1中,t11 和 t12 已经停止,进程p2 的t21 停止。当前线程是t11。如果运行continue命令,调试器作者可以选择,要么仅仅resume t11,或者resume p1中的所有stopped的线程,或者resume所有停止的线程。

上边的这些例子,就是想说,面对多进程多线程的挑战的时候,调试器需要能够灵活的控制每个命令的范围,而描述这个范围的就是ITSET,但是,如何能够是调试器能够按照ITSET的内容,来准确的控制线程和进程,就是调试器自己的事情了,这也就是现在GDB要做的事情。

ITSET是什么样子的?

如上边介绍,ITSET就是一个线程 进程 核 的集合,所以语法和现有的数学集合很像。”-” 是范围,比如 “t1-4″ 线程1到4,”,” 是或操作 “.” 是与操作等等。这里不详细介绍ITSET的语法,因为每个实现对ITSET都有一些微小的修改。我下来会给一些例子,让大家感受一下ITSET,

    i1.t1-4 inferior 1中的thread 1到4
    i1.t1,i2.t2 inferior 1中的thread 1 或者 inferior 2 中的thread 2
    running.c0 所有在core 0上处于running 状态的线程

有了这样的语法,我们就能轻松灵活的控制命令的范围了。

用ITSET控制命令范围

在将来的GDB实现中,可能是用 itfocus 命令来控制某个命令的作用范围的。

itfocus ITSET command

比如,可以这样控制命令的范围 (下边的例子中的命令还不存在于GDB中)

    itfocus i1.t1-2 trace foo在函数foo上设置tracepoint,但是只有对inferior 1中的 t1和t2 有效。也就是说,这个tracepoint是 thread specific的。
    itfocus t1-2 p/x var打印变量var在线程 t1 和 t2 的value的value。如果var是一个局部变量,线程t1和t2若在不同的stack上,var的value是不一样的。
    itfocus stopped.~t1-2 stepi让除了t1 t2 的所有处于stopped状态的线程单步执行一条指令。

这样的例子会有很多,大家应该能够从这样的例子里边,感受到ITSET对调试器的提高。

结束语
本文介绍了可能在GDB出现的新功能ITSET,以及ITSET的简单语法,初步体现了ITSET的灵活和强大。有了ITSET的GDB,将会对线程 进程的控制和命令 有更加细粒度,使用起来更加灵活。但是,这样的灵活性本身并没有解决多线程程序的复杂,相反,这样的灵活性,可能是用户更加的困惑。总之,ITSET只是一个手段, 还是需要用户来自己解决自己的问题。当程序的线程数目和处理器的core的数目继续增长,纵然有ITSET,用户也是无能为力。所以,更加智能的分析,才是用户更需要的。

 

Copyright (c) 2012 陳韋任 (Chen Wei-Ren)

最後,如同我之前所承諾的。我們來看 tlb_fill 從一般 C 函式和 code cache 被呼叫是什麼意思。

我們可以看到 retaddr == 0 時,tlb_fill 是從一般 C 函式被呼叫。

(gdb) b tlb_fill
(gdb) r -boot a -fda Image -hda hdc-0.11-new.img -vnc 0.0.0.0:1 -d in_asm,op,out_asm
Breakpoint 1, tlb_fill (addr=4294967280, is_write=2, mmu_idx=0, retaddr=0x0) at
/tmp/chenwj/qemu-0.13.0/target-i386/op_helper.c:4816
4816    {
(gdb) bt
#0  tlb_fill (addr=4294967280, is_write=2, mmu_idx=0, retaddr=0x0) at /tmp/chenwj/qemu-0.13.0/target-i386/op_helper.c:4816
#1  0x000000000050ee86 in __ldb_cmmu (addr=4294967280, mmu_idx=0) at /tmp/chenwj/qemu-0.13.0/softmmu_template.h:134
#2  0x000000000051045e in ldub_code (ptr=4294967280) at /tmp/chenwj/qemu-0.13.0/softmmu_header.h:87
#3  0x000000000051054b in get_page_addr_code (env1=0x110e390, addr=4294967280) at /tmp/chenwj/qemu-0.13.0/exec-all.h:325
#4  0x0000000000510986 in tb_find_slow (pc=4294967280, cs_base=4294901760, flags=68) at /tmp/chenwj/qemu-0.13.0/cpu-exec.c:139
#5  0x0000000000510b9d in tb_find_fast () at /tmp/chenwj/qemu-0.13.0/cpu-exec.c:188
#6  0x00000000005112db in cpu_x86_exec (env1=0x110e390) at /tmp/chenwj/qemu-0.13.0/cpu-exec.c:575
#7  0x000000000040aabd in qemu_cpu_exec (env=0x110e390) at /tmp/chenwj/qemu-0.13.0/cpus.c:767
#8  0x000000000040abc4 in cpu_exec_all () at /tmp/chenwj/qemu-0.13.0/cpus.c:795
#9  0x000000000056e417 in main_loop () at /tmp/chenwj/qemu-0.13.0/vl.c:1329
#10 0x00000000005721cc in main (argc=11, argv=0x7fffffffe1a8, envp=0x7fffffffe208) at /tmp/chenwj/qemu-0.13.0/vl.c:2992

我們可以看到 retaddr != 0 時,tlb_fill 是從 code cache 中被呼叫。

Breakpoint 1, tlb_fill (addr=28668, is_write=1, mmu_idx=0, retaddr=0x4000020c)
at /tmp/chenwj/qemu-0.13.0/target-i386/op_helper.c:4816
4816    {
(gdb) bt
#0  tlb_fill (addr=28668, is_write=1, mmu_idx=0, retaddr=0x4000020c) at /tmp/chenwj/qemu-0.13.0/target-i386/op_helper.c:4816
#1  0x000000000054e511 in __stl_mmu (addr=28668, val=982583, mmu_idx=0) at /tmp/chenwj/qemu-0.13.0/softmmu_template.h:272
#2  0x000000004000020d in ?? ()  <--- 我們在 code cache 裡!

我們來看一下 qemu.log 驗證一下我們對 QEMU 的了解。;) 既然 retaddr = 發生例外的 host binary 下一條指令位址減去 1,我們定位到 0x4000020d。

0x40000208:  callq  0x54e3a0
0x4000020d:  movzwl %bx,%ebp

瞧瞧 __stl_mmu 的位址,果然是 0x54e3a0。這代表我們在 code cache 呼叫 __stl_mmu。__stl_mmu 再去呼叫 tlb_fill 的時候發生例外。

(gdb) p __stl_mmu
$1 = {void (target_ulong, uint32_t, int)} 0x54e3a0

這裡我們可以看到 SOFTMMU 相關的 helper function 在各個地方都會被用到,不論是 QEMU 本身的函式 (一般 C 函式) 或是 code cache 都會調用 __{ld,st}{b,w,l,q}_{cmmu,mmu}。這些 helper function 又會調用 tlb_fill。tlb_fill 就是透過 retaddr 來判定是否需要回復 guest CPUState。

 

Copyright (c) 2012 陳韋任 (Chen Wei-Ren)

好! 我們現在找到例外 (本範例是頁缺失) 是發生在某個 TranslationBlock 裡頭,但是到底是哪一條 guest 指令觸發頁缺失? 我們需要從頭翻譯該 TranslationBlock 對應的 guest binary 來揪出罪魁禍首。一般情況下,QEMU 在翻譯 guest binary 時不會記錄 guest pc 資訊。這時,為了定位 guest pc,QEMU 在翻譯 guest binary 會記錄額外的資訊,包含 guest pc。

QEMU 會用到底下定義在 translate-all.c 資料結構:

  target_ulong gen_opc_pc[OPC_BUF_SIZE]; // 紀錄 guest pc。
  uint8_t gen_opc_instr_start[OPC_BUF_SIZE]; // 當作標記之用。

針對 x86,又在 target-i386/translate.c 定義以下資料結構:

  static uint8_t gen_opc_cc_op[OPC_BUF_SIZE]; // 紀錄 condition code。

現在來看 cpu_restore_state (translate-all.c)。searched_pc 傳入的 (幾乎) 是發生例外的 host pc。

int cpu_restore_state(TranslationBlock *tb,
                      CPUState *env, unsigned long searched_pc,
                      void *puc)
{
    tcg_func_start(s); // 初始 gen_opc_ptr 和 gen_opparam_ptr
 
    // 轉呼叫 gen_intermediate_code_internal,要求在生成 TCG IR
    // 的同時,為其生成相關的 guest pc 和其它資訊於下列資料結構。
    //
    //   gen_opc_pc, gen_opc_instr_start, 和 gen_opc_cc_op
    //
    gen_intermediate_code_pc(env, tb);
 
    // 轉呼叫 tcg_gen_code_common (tcg/tcg.c) 將 TCG IR 翻成 host binary。
    // 返回 TCG gen_opc_buf index。
    j = tcg_gen_code_search_pc(s, (uint8_t *)tc_ptr, searched_pc - tc_ptr);
 
    // gen_opc_instr_start[j] 若為 1,代表 gen_opc_pc[j] 和 gen_opc_cc_op[j]
    // 正是我們所要的資訊。
    while (gen_opc_instr_start[j] == 0)
        j--;
 
    // 回復 CPUState。
    gen_pc_load(env, tb, searched_pc, j, puc);
 
}

gen_intermediate_code_pc 是 gen_intermediate_code_internal 的包裝,search_pc 設為 1。當 search_pc 為 true,在翻譯 guest binary 的同時,生成額外資訊。

static inline void gen_intermediate_code_internal(CPUState *env,
                                                  TranslationBlock *tb,
                                                  int search_pc)
{
    // guest binary -&gt; TCG IR
    for(;;) {
 
        if (search_pc) {
            // gen_opc_ptr 為 TCG opcode buffer 目前位址,gen_opc_buf 為
            // TCG opcode buffer 的起始位址。
            j = gen_opc_ptr - gen_opc_buf;
            if (lj &lt; j) {
                lj++;
                while (lj cc_op; // 紀錄 condition code。
            gen_opc_instr_start[lj] = 1; // 填 1 作為標記。
            gen_opc_icount[lj] = num_insns;
        }
 
        // 針對 pc_ptr 代表的 guest pc 進行解碼並生成 TCG IR,返回下一個 guest pc。
        pc_ptr = disas_insn(dc, pc_ptr);
 
    }
}

tcg_gen_code_search_pc 是 tcg_gen_code_common 的包裝,search_pc (應命名為 offset) 設為發生例外的 host binary 與其所屬 basic block 在 code cache 開頭 (tc_ptr) 的 offset。注意! 此時傳入 gen_code_buf 的是觸發例外的 TranslationBlock 其 tc_ptr。也就是說,現在 TCG IR -> host binary 中的 host binary 是寫在發生例外 host binary 所屬 basic block 在 code cache 的開頭。我們把這段 host binary 覆寫了! 當然寫的內容和被覆寫的內容一模一樣。我們只想要透過這個方式反推觸發例外的 guest pc。

static inline int tcg_gen_code_common(TCGContext *s, uint8_t *gen_code_buf,
                                      long search_pc)
{
    for(;;) {
        switch(opc) {
        case INDEX_op_nopn:
            args += args[0];
            goto next;
        case INDEX_op_call:
            dead_args = s-&gt;op_dead_args[op_index];
            args += tcg_reg_alloc_call(s, def, opc, args, dead_args);
            goto next;
        }
        args += def-&gt;nb_args;
    next:
        // 如果 offset (search_pc) 落在 tc_ptri (gen_code_buf) 和 code cache
        // 目前存放 host binary 的位址之間, 返回 TCG gen_opc_buf index。
        if (search_pc &gt;= 0 &amp;&amp; search_pc <s>code_ptr - gen_code_buf) {
            return op_index;
        }
        op_index++;
    }
}

此時,gen_opc_pc 和 gen_opc_cc_op 已存放發生例外的 guest pc 和當時的 condition code。gen_pc_load 負責回復 CPUState。

void gen_pc_load(CPUState *env, TranslationBlock *tb,
                unsigned long searched_pc, int pc_pos, void *puc)
{
    env-&gt;eip = gen_opc_pc[pc_pos] - tb-&gt;cs_base;
    cc_op = gen_opc_cc_op[pc_pos];
}

至此,CPUState 已完全回復,我們回來看 tlb_fill。raise_exception_err (target-i386/op_helper.c) 這時候拉起虛擬 CPU 的 exception_index (env->exception_index),並設置 error_code (env->error_code)。

void tlb_fill(target_ulong addr, int is_write, int mmu_idx, void *retaddr)
{
    ret = cpu_x86_handle_mmu_fault(env, addr, is_write, mmu_idx, 1);
    if (ret) {
        if (retaddr) {
 
            // 當客戶發生頁缺失 (ret == 1) 且 tlb_fill 是從 code cache 中被
            // 呼叫 (retaddr != 0),我們會在這裡。
 
            /* now we have a real cpu fault */
            pc = (unsigned long)retaddr;
            tb = tb_find_pc(pc);
            if (tb) {
                /* the PC is inside the translated code. It means that we have
                   a virtual CPU fault */
                cpu_restore_state(tb, env, pc, NULL);
            }
        }
        raise_exception_err(env-&gt;exception_index, env-&gt;error_code);
    }
    env = saved_env;
}

raise_exception_err 實際上是 raise_interrupt 的包裝 (wrapper)。QEMU_NORETURN 前綴代表此函式不會返回。它其實是 GCC 擴展 __attribute__ ((__noreturn__)),定義在 qemu-common.h [1]。

static void QEMU_NORETURN raise_interrupt(int intno, int is_int, int error_code,
                                          int next_eip_addend)
{
    ... 略 ...
 
    env-&gt;exception_index = intno;
    env-&gt;error_code = error_code;
    env-&gt;exception_is_int = is_int;
    env-&gt;exception_next_eip = env-&gt;eip + next_eip_addend;
    cpu_loop_exit();
}

cpu_loop_exit (cpu-exec.c) 用 longjmp 返回至 cpu_exec (cpu-exec.c) 中處理例外的分支。

void cpu_loop_exit(void)
{
    env-&gt;current_tb = NULL;
    longjmp(env-&gt;jmp_env, 1);
}

來看 cpu_exec。cpu_exec 裡用到許多 #ifdef,強烈建議查看經過預處理之後結果,即 ${BUILD}/i386-softmmu/cpu-exec.i 中的 cpu_x86_exec。

int cpu_exec(CPUState *env)
{
    // 進行翻譯並執行的迴圈。
    /* prepare setjmp context for exception handling */
    for(;;) {
        if (setjmp(env-&gt;jmp_env) == 0) { // 正常流程。
            /* if an exception is pending, we execute it here */
            if (env-&gt;exception_index &gt;= 0) {
 
              /* 2. 再來到這裡,處理例外。 */
 
            }
 
            next_tb = 0; /* force lookup of first TB */
            for(;;) {
 
            } /* inner for(;;) */
        }
 
        /* 1. 我們先來到這裡。 */
 
    } /* outer for(;;) */
}

O.K.,到這裡就是一個循環。:-) 接著,我們來驗證一下我們對 QEMU 的理解。

[1] http://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html

 

Copyright (c) 2012 陳韋任 (Chen Wei-Ren)

首先我們來看 __stb_mmu。請看 ${SRC}/softmmu_template.h 和 ${BUILD}/i386-softmmu/op_helper.i。SOFTMMU 相關的 helper function 是透過 softmmu_* 檔案內的巨集加以合成。這裡只挑部分加以描述。

SUFFIX 可以是 b (byte, 8)、w (word, 16)、l (long word, 32) 和 q (quad word, 64),代表資料大小。MMUSUFFIX 可以是 cmmu 或是 mmu,分別代表欲讀取的是 code 或是 data。mmu_idx 代表索引的是內核態亦或是用戶態的 TLB。addr 代表 guest virtual address。

// ${SRC}/softmmu_template.h
void REGPARM glue(glue(__st, SUFFIX), MMUSUFFIX)(target_ulong addr,
                                                 DATA_TYPE val,
                                                 int mmu_idx)
{
   ...
}

接著看展開巨集後的函式體。

// ${BUILD}/i386-softmmu/op_helper.i
void __stb_mmu(target_ulong addr, uint8_t val, int mmu_idx)
{
 redo:
    // 查找 TLB
    tlb_addr = env-&gt;tlb_table[mmu_idx][index].addr_write;
    if (...) {
 
        // TLB 命中
 
    } else {
 
        // TLB 不命中
 
        /* the page is not in the TLB : fill it */
        // retaddr = GETPC();
        retaddr = ((void *)((unsigned long)__builtin_return_address(0) - 1));
 
        // 試圖填入 TLB entry。
        tlb_fill(addr, 1, mmu_idx, retaddr);
        goto redo;
    }
}

這裡 QEMU 利用 GCC 的 __builtin_return_address 擴展 [1] 來判定 tlb_fill 是從一般 C 函式或是 code cache 中被呼叫。retaddr 若為 0,表前者,retaddr 若不為 0,表後者。之後,我們會透過 GDB 更加清楚前面所述所代表的意思。我們關注 retaddr 不為 0,也就是從 code cache 中呼叫 tlb_fill 的情況。

在看 tlb_fill 之前,我們先偷看 cpu_x86_handle_mmu_fault (target-i386/helper.c) 的註解。我們關注返回值為 1,也就是頁缺失的情況。

/* return value:
   -1 = cannot handle fault
   0  = nothing more to do
   1  = generate PF fault
*/
int cpu_x86_handle_mmu_fault(CPUX86State *env, target_ulong addr, ...)
{
  ...
}

我們來看 tlb_fill。

void tlb_fill(target_ulong addr, int is_write, int mmu_idx, void *retaddr)
{
    ret = cpu_x86_handle_mmu_fault(env, addr, is_write, mmu_idx, 1);
    if (ret) {
        if (retaddr) {
 
            // 當客戶發生頁缺失 (ret == 1) 且 tlb_fill 是從 code cache 中被
            // 呼叫 (retaddr != 0),我們會在這裡。
 
            /* now we have a real cpu fault */
            pc = (unsigned long)retaddr;
            tb = tb_find_pc(pc);
            if (tb) {
                /* the PC is inside the translated code. It means that we have
                   a virtual CPU fault */
                cpu_restore_state(tb, env, pc, NULL);
            }
        }
        raise_exception_err(env-&gt;exception_index, env-&gt;error_code);
    }
    env = saved_env;
}

請注意! 如果 retaddr != 0,其值代表的 (幾乎) 是發生例外的 host binary 所在位址。QEMU 利用它來查找是哪一個 TranslationBlock 中的 host binary 發生例外。tb_find_pc (exec.c) 利用該 host binary pc 進行查找,取得 tb。

TranslationBlock *tb_find_pc(unsigned long tc_ptr)
{
    // tbs 是 TranslationBlock * 數組。每一個在 code cache 中 (已翻譯好的)
    // basic block 都有相對應的 TranslationBlock 存放其相關資訊。
 
    /* binary search (cf Knuth) */
    m_min = 0;
    m_max = nb_tbs - 1;
    while (m_min &gt; 1;
        tb = &amp;tbs[m];
        // tc_ptr 代表 host binary 在 code cache 的起始位址。
        v = (unsigned long)tb-&gt;tc_ptr;
        if (v == tc_ptr)
            return tb;
        else if (tc_ptr &lt; v) {
            m_max = m - 1;
        } else {
            m_min = m + 1;
        }
    }
    return &amp;tbs[m_max];
}

一但找到該負責的 tb,QEMU 就會回復 guest CPUState 以便 guest exception handler 處理 guest 的頁缺失例外。

    if (tb) {
        /* the PC is inside the translated code. It means that we have
           a virtual CPU fault */
        cpu_restore_state(tb, env, pc, NULL);
    }

接著我們看 cpu_restore_state (translate-all.c)。

[1] http://gcc.gnu.org/onlinedocs/gcc/Return-Address.html

 

Copyright (c) 2012 陳韋任 (Chen Wei-Ren)

這裡以 linux-0.11 當範例,請至 [1][2] 下載代碼和硬盤映像編譯。我們觀察以 guest pc 0xe4c0 為開頭的 basic block。使用 QEMU 0.13 [3] 運行 linux-0.11。

$ mkdir build; cd build
$ ../qemu-0.13.0/configure --prefix=$INSTALL --target-list=i386-softmmu \
    --enable-debug --extra-cflags="--save-temps"
$ make install
$ gdb qemu
(gdb) r -boot a -fda Image -hda hdc-0.11-new.img -vnc 0.0.0.0:1 -d in_asm,op,out_asm

登入後,下 `ls`。觀察 qemu.log 並定位至 0xe4c0。首先,我們可以看到如下內容:

IN:
0x0000e4c0:  sub    $0x4,%esp
0x0000e4c3:  mov    0x8(%esp),%eax
0x0000e4c7:  mov    %al,(%esp)
0x0000e4ca:  movzbl (%esp),%eax
0x0000e4ce:  mov    0xc(%esp),%edx
0x0000e4d2:  mov    %al,%fs:(%edx)
0x0000e4d5:  add    $0x4,%esp
0x0000e4d8:  ret

OP:
 ---- 0xe4c0
 movi_i32 tmp1,$0x4
 mov_i32 tmp0,esp
 sub_i32 tmp0,tmp0,tmp1
 mov_i32 esp,tmp0
 mov_i32 cc_src,tmp1
 mov_i32 cc_dst,tmp0

 ... 略 ...

OUT: [size=450]
0x40bbeff0:  mov    0x10(%r14),%ebp
0x40bbeff4:  sub    $0x4,%ebp

 ... 略 ...

0x4011813b:  callq  0x54d38a
0x40118140:  mov    0x10(%r14),%ebp

 ... 略 ...

這是 QEMU 第一次遇到尚未翻譯,以 guest pc 0xe4c0 開頭的 basic block 時所產生的輸出,這包括 guest binary (IN: 以下內容)、TCG IR (OP: 以下內容) 和 host binary (OUT: 以下內容)。

再繼續往下搜尋 0xe4c0,會看到以下內容:

IN:
0x0000e4c0:  sub    $0x4,%esp
0x0000e4c3:  mov    0x8(%esp),%eax
0x0000e4c7:  mov    %al,(%esp)
0x0000e4ca:  movzbl (%esp),%eax
0x0000e4ce:  mov    0xc(%esp),%edx
0x0000e4d2:  mov    %al,%fs:(%edx)
0x0000e4d5:  add    $0x4,%esp
0x0000e4d8:  ret

OP:
 ---- 0xe4c0
 movi_i32 tmp1,$0x4
 mov_i32 tmp0,esp
 sub_i32 tmp0,tmp0,tmp1
 mov_i32 esp,tmp0
 mov_i32 cc_src,tmp1
 mov_i32 cc_dst,tmp0

 ... 略 ...

RESTORE:
0x0000: 0000e4c0
0x0007: 0000e4c3
0x000d: 0000e4c7
0x0011: 0000e4ca
0x0015: 0000e4ce
0x001b: 0000e4d2
spc=0x4011813f pc_pos=0x1b eip=0000e4d2 cs_base=0

這裡就是重點了。spc 指的是發生例外的 host pc,eip 指的是與其相對映發生例外的 guest pc。這邊請注意,由於我們將 guest binary 翻譯成 host binary 並執行,真正發生例外的是 host binary (位於 code cache),但是我們必須將它映射回 guest pc,查出哪一條 guest 指令發生例外,並做後續處理。我們看一下第一次翻譯 0xe4d2 所得的 host binary。

0x4011813b:  callq  0x54d38a
0x40118140:  mov    0x10(%r14),%ebp

我們可以看到 spc 0x4011813f == 0×40118140 – 1,也就是 callq 0x54d38a 下一條指令所在位址減去 1。這裡做點弊,我們在 gdb 下 print __stb_mmu。

(gdb) print __stb_mmu
$1 = {void (target_ulong, uint8_t, int)} 0x54d38a

可以得知,我們在呼叫 __stb_mmu 的時候發生例外。__{ld,st}{b,w,l,q}_{cmmu,mmu} 是用來存取 guest 內存的 helper function。它們首先會查找 TLB (env1->tlb_table) 試圖取得 guest virtual address 相對映的 host virtual address。如果 TLB 命中,可直接利用該 host virtual address 存取 guest 內存內容。如果 TLB 不命中,則會呼叫 tlb_fill (target-i386/op_helper.c)。tlb_fill 會呼叫 cpu_x86_handle_mmu_fault 查找客戶頁表。如果命中,代表該 guest virtual address 所在的頁已存在,tlb_fill 會將該頁項目填入 TLB 以便後續查找。如果不命中,代表發生頁缺失,QEMU 會回復 guest CPUState,並拉起 guest exception index (env->exception_index) 通知 guest 頁缺失發生。最後交由 guest 頁缺失 handler 將該頁載入。

我先給出一個 precise exception handling 的大致流程,之後再透過閱讀代碼有更深的體會。底下給出關鍵的資料結構 TranslationBlock,它負責掌管 guest binary 和 host binary 的關係,其中 pc 代表此 basic block 起始的 guest pc,tc_ptr 指向翻譯好的 host binary 在 code cache 中的位址。

                                                                     code cache
          guest binary                   TranslationBlock           (host binary)

0x0000e4c0:  sub    $0x4,%esp         0x40bbeff0:  mov 0x10(%r14),%ebp
0x0000e4ca:  movzbl (%esp),%eax                                 0x40bbeff4:  sub $0x4,%ebp
0x0000e4ce:  mov    0xc(%esp),%edx
0x0000e4d2:  mov    %al,%fs:(%edx)                                      ... 略 ...
0x0000e4d5:  add    $0x4,%esp
0x0000e4d8:  ret                                                0x4011813b: callq  0x54d38a
                                               (3) offset  -->  0x40118140: mov    0x10(%r14),%ebp (1)

                                                                        ... 略 ...

當 QEMU 發現例外是發生在 code cache 裡,這代表需要處理 precise exception。首先,QEMU 會透過 host pc (0x4011813f) 查出相對應的 TranslationBlock – (1)。接下來,QEMU 會回復 guest CPUState。主要概念是這樣,透過之前查找到的 TranslationBlock 的 pc,我們從該 pc 所指的 guest binary 重新再翻譯一次,同時產生額外的資訊以便回復 guest CPUState – (2)。那要如何得知我們已經翻譯到發生 exception 的 guest binary? 這裡的重點在於,我们重新翻译 guest binary,直到 host binary 地址到达了出现异常的位置,这个时候 guest pc 就是产生异常的指令。我們便可以回復 guest CPUState – (3)。

接下來各小節的主題分別是:

* Precise Exception Handling 3/5 – (1)

* Precise Exception Handling 4/5 – (2) (3)

* Precise Exception Handling 5/5 – 驗證我們對 QEMU 的理解。

接下來,我們來看代碼。;)

[1] http://www.oldlinux.org/oldlinux/viewthread.php?tid=13681&extra=page%3D1
[2] http://oldlinux.org/Linux.old/bochs/linux-0.11-devel-060625.zip
[3] http://wiki.qemu.org/download/qemu-0.13.0.tar.gz

 

Copyright (c) 2012 陳韋任 (Chen Wei-Ren)

一般計算機架構會定義當例外/中斷發生時,當下的 CPU 狀態應當為何,這個稱之為 precise exception。假設當下流水線是底下這樣:

  A. addl %eax,(%esp)
  B. addl %ebx,(%esp)
  C. movl %esi,(%ebp)
  D. subl %ecx,5

當執行到指令 (C),欲從 %ebp 所指的內存位址讀取資料至 %esi 時,發生頁缺失例外。以 x86 對 precise exception 的定義,在指令 (C) 之前的指令,即 (A) 和 (B) 其結果必須完成。也就是說,暫存器/內存的內容應該更新; 在指令 (C) 之後的指令,即 (D) 的結果必須捨棄。此範例取自 [1]。(註: 對此段匯編的解讀,我直接引用 [1]。如果單從 AT&T 語法來看,指令 (C) 應為將 %esi 其值寫至 %ebp 所指位址。)

Precise exception 在 binary translation 中佔有重要地位。在此以 QEMU 為例,說明 QEMU 如何確保 pecise exception。由以上對 pecise exception 的說明,我們可以知道 precise exception 必須考量暫存器和內存。在 binary translation 中,我們關注的是客戶 (guest) 的 precise exception。因此,我們必須確保當 guest 代碼發生例外時,guest 的暫存器和內存其內容必須滿足 precise exception 的要求。這樣 guest 的 exception handler 才能正確處理該例外。

就 guest 暫存器而言,在 QEMU 中需要考量的是 CPUState。QEMU 在每一個可能會發生例外的指令或是 helper function 之前,會將 CPUState 中的大部分內容更新,少數未更新的內容會在 guest 真正發生例外時重新再計算。以 x86 為例,pc 和 condition code 屬於後者。在 binary translation 中,出於效能上的考量,通常會在一個 basic block 的結尾才更新 guest pc,而非每翻譯一個 guest 指令就更新 guest pc。

就 guest 內存而言,我們將重點放在 guest memory store operation 上,因為只有 memory store operation 會修改 guest 內存的內容。第一,QEMU 會依照 guest 原本 memory store operation 的順序進行翻譯。第二,針對所有潛在會發生例外的 guest 指令,QEMU 保留其相對於 guest memory store operation 的順序。簡單來說,QEMU 不會 reorder guest 指令順序。這簡化了 QEMU 維護 precise exception 的複雜度,但同時也喪失了一些潛在可能的優化 [2][3]。硬體对异常的处理可能还有别的行为,只要滿足 precise exception 的規範,同样是可以接受的。在 QEMU,我们选择了一个最为保守的实现方式。

接下來以 guest 頁缺失例外為例,讓我們來觀察當 guest 發生頁缺失時,QEMU 是如何維護正確的 guest 暫存器和內存。

[1] The Technology Behind Crusoe™ Processor
[2] http://lists.gnu.org/archive/html/qemu-devel/2012-02/msg04138.html
[3] http://people.cs.nctu.edu.tw/~chenwj/log/QEMU/agraf-2012-03-02.txt

 

Copyright (c) 2012 陳韋任 (Chen Wei-Ren)

緣起

在 LLVM 郵件列表上看到一封介紹一款程式語言 Julia,後端基於 LLVM JIT [1]。Julia 的目標非常宏大,它想要擁有當今大多數語言有的優點,卻沒有其缺點。我對它的目標不做評論。在介紹作者們為何要創造 Julia 這一語言的文章中,我注意到 CSDN 有篇中文翻譯,翻得有點拗口。這類的技術文章通常都需要有一定背景知識的人才能較好的掌握原作者的意思。在此,獻上此篇中文翻譯。限於個人能力有限,歡迎不吝批評指教。

原文: Why We Created Julia

簡單來說,因為我們很貪心。

我們是重度 Matlab 使用者。其中有些人是 Lisp 黑客,有些人是 Python 愛好者,其他人是 Ruby 愛好者,甚至有人是 Perl 黑客。我們其中有些人在我們毛還沒長齊之前就在使用 Mathematica。有些人甚至是女性。我們使用 R 語言產生許多統計圖型。C 是我們最為喜愛的語言 [2]。

我們熱愛所有這些語言,它們是如此美妙且強大。就我們所工作的領域 – 科學計算,機器學習,資料探勘,大規模線性計算,分散與平行計算 – 每一種語言對某些領域來說堪稱完美,但對其它領域卻是惡夢。使用一種語言都是一種權衡的藝術。

我們很貪心,我們想要更多。

我們想要一種語言,它必須是開源的,採自由授權。我們希望 C 的速度,Ruby 的動態性。我們想要一種語言,其資料和代碼同一格式 (homoiconic),同 Lisp 一般擁有真的巨集,但卻使用像 Matlab 那樣明顯和熟悉的數學符號來加以表示。我們想要一種像 Python 一樣對於各種領域問題都如此好用 (general programming) 的語言。對於統計方面,像 R 一樣容易使用。對於字串處理,又像使用 Perl 一般自然。對於線性代數,像 Matlab 般強大。像 shell 一樣,可以用來膠合程序中其它的組件。易於學習,卻又能讓最為嚴肅的黑客高興。我們希望它是互動式,又同時是編譯式的語言。

(我們有提到它必須有 C 一般的速度,對吧?)

我們是苛刻的,我們想要一種語言能提供如 Hadoop 那樣強大的分散式計算 – 沒有臃腫的 Java 和 XML 代碼; 不需要被強迫去過濾放在數以百計機器上的海量日誌來找出臭蟲。我們想要語言有強大的表達能力,卻又沒有令人費解的複雜性。我們想要寫出一個簡單、做純量計算的迴圈,它可以被編譯成只使用到暫存器的機器碼。我們想要寫出 A * B 的矩陣計算,並同時在數千台機器上發起數千個計算,計算龐大的矩陣乘積。

我們不想提到型別,當我們不喜歡它的時候。但當我們需要多型函式 (polymorphic function),我們希望使用泛型編程 (generic programming) 撰寫演算法,只寫一次,並將該演算法套用在無窮多的型別上。我們希望使用多分派 (multiple dispatch) 來有效的根據函式所有參數選擇一個最適當的實現,並為截然不同的型別提供共通的功能。僅管擁有如此強大的能力,我們希望這個語言能簡單且乾淨。

我們似乎要求太多了,對嗎?

即使我們認識到我們是無可救藥的貪婪,我們仍舊想要上述所有的特性。大約在兩年半以前,我們開始創造一種滿足我們貪婪的語言。它還不完備,但是該是時候發布 1.0 正式版了 – 我們所創造的語言叫做 Julia。它已經滿足我們 90% 無理的要求,現在它需要其他人無理的要求讓它更完美。所以,如果你同樣也是一個貪婪、不可理喻、苛刻的程序員,我們希望你前來一試。

[1] Introducing julia, and gauging interest in a julia BOF session at the upcoming LLVM conference in London
[2] http://people.cs.nctu.edu.tw/~chenwj/log/LLVM/jey-2012-03-08.txt

 

Copyright (c) 2012 陳韋任 (Chen Wei-Ren)

人總是要經由教訓才能真正學到東西。我在函数声明的重要性上深有體會。這裡給出我血淋淋的教訓

情境描述:

在 x86_64 的機器上,指針 (pointer) 大小為 64 bit。我自定一個函式如下,

  TranslationBlock *my_tb_find_pc(unsigned long tc_ptr);

但是在使用上述函式的時候,我沒有先宣告其 prototype。

void tlb_fill(target_ulong addr, int is_write, int mmu_idx, void *retaddr)
{
         ...
 
    TranslationBlock *tb_ptr = my_tb_find_pc(pc);
 
         ...
}

在用 GDB 進到 my_tb_find_pc 觀察其返回值時,該值為 0x7fffe41b22d8。但回到 tlb_fill 的時候,發現 tb_ptr 被賦值 0xffffffffe41b22d8。這詭異的情況可以用以下流程解釋:

  0x7fffe41b22d8 -> (int32_t ) 0xe41b22d8 -> (int64_t) 0xffffffffe41b22d8

0x7fffe41b22d8 先被轉成 32 bit 的 int,再 sign extend 成 64 bit 的 int。這邊問題就在於根據 C90,編譯器會將沒有顯示宣告 prototype 的函式其返回值型別視為 int (32 bit)。這就會造成上述詭異的情況。開啟 -Wall 編譯會得到以下警告:

  warning: implicit declaration of function 'my_tb_find_pc'
  warning: assignment makes pointer from integer without a cast

其訊息描述了編譯器將沒有顯式宣告 prototype 的 my_tb_find_pc 其返回值型別視為 int,再將其轉為指針 (pointer)。

© 2012 HelloGCC -- Open Source Toolchain Blog Suffusion theme by Sayontan Sinha