在Windows上使用Wilson

之前被NS老兄激起了兴趣发过 beef帖,主要是显示可以很容易的写出Ruby扩展用于直接生成机器码,连接到Ruby的对象系统上,然后像调用普通Ruby方法一样去使用。我觉得这个很有趣,想看看有没有好的办法做个internal DSL出来在Ruby里写类似MASM语法的汇编,然后让Bk201生成机器码出来执行。

昨天看到某帖之后又把我的兴趣激起来了……在动手实现DSL之前,我先搜了一下Ruby assembler,发现Ryan Davis写了个叫 Wilson的库,功能跟我想要的很类似,虽然语法有点不同,好歹还是NASM而不是AT&T式的。当前版本是1.1.1。我现在是在Windows XP SP3/Ruby 1.8.6上测试的。

安装该库用:
gem install wilson

加载该库用require 'wilson'。它会对Ruby一些核心类做monkey patch,要注意。

原本Wilson是为在Mac上使用而编写,但整个代码都是纯Ruby的,所以在Windows上使用稍微修改一下平台相关的代码就行。在ruby安装目录的gems/1.8/gems/wilson-1.1.1/lib/目录下,
wilson.rb,第1135-1136行:
  dir = File.join(Config::CONFIG["prefix"], "lib")
  dlload File.join(dir, "libruby.dylib")

.dylib是Mac上的动态链接库文件的后缀。这里要改为:
  dir = File.join(Config::CONFIG["prefix"], "bin")
  dlload File.join(dir, Config::CONFIG["LIBRUBY_SO"])

就可以在Windows上正常使用了。

Wilson在一些测试里会使用到RubyInline,有兴趣的话也可以装上:
gem install rubyinline

加载该库用require 'inline'。


如果把MASM语法下op dest, src的形式看作前缀表达式,那么Wilson的DSL表现出来的就是dest.op src形式的“中缀表达式”。所以原本写作xor eax, eax的汇编,在Wilson就写作eax.xor eax。习惯了之后这么写也挺顺的。
让我们来看看Wilson in action~
require 'rubygems'
require 'wilson'

class A
  defasm :add, :a, :b do
    a, b = arg(0), arg(1)
    eax.mov a
    eax.add b
    eax.dec
  end
  
  defasm :ary_len, :ary do
    ary = arg(0)
    eax.mov ary
    eax.add 4*2           # skip RBasic.flags and RBasic.klass
    eax.mov eax.m
    to_ruby eax
  end
  
  defasm :ary_store, :ary, :idx, :val do
    ary, idx, val = arg(0), arg(1), arg(2)
    eax.mov ary
    ecx.mov(eax + 4*4)    # address of RArray.ptr
    edx.mov idx
    from_ruby edx
    edx.shl 2
    ecx.add edx
    edx.mov val
    ecx.m.mov edx
  end
end

a = A.new
p a.add(3, 5)             #=> 8
p a.ary_len([])           #=> 0
p a.ary_len([2, 4, 6, 8]) #=> 4
ary = [1, 2, 3]
a.ary_store(ary, 2, 4)    #=> [1, 2, 4]
p ary                     #=> [1, 2, 4]

Wilson的实现暂时还不太完善,例如说声明一个汇编方法用defasm,其参数包括生成方法的名字和参数列表(的名字),但在定义方法体时却暂时还无法用名字与引用参数,只能通过arg(0)、arg(1)之类的形式去引用。寻址模式中放大倍数的版本也都还没实现,上面代码中的ary_store本来可以用lea ecx, dword ptr [ecx + edx*4]一条指令完成一个乘法和一个加法,但Wilson还不支持edx*4的形式,非要用的话只好如下所示:
class A
  defasm :ary_store, :ary, :idx, :val do
    ary, idx, val = arg(0), arg(1), arg(2)
    eax.mov ary
    ecx.mov(eax + 4*4)  # RArray.ptr
    edx.mov idx
    from_ruby edx
    # ecx.lea(ecx + edx*4) # this is not supported by Wilson yet
    # hack: lea  ecx, dword ptr [ecx + edx*4]
    self.stream.concat [0x8D, 0x0C, 0x91]
    edx.mov val
    ecx.m.mov edx
  end
end

把机器码硬插到stream里……OTL

另外Wilson在把某些指令汇编到机器码时有问题。我试过跟ebx相关的几条指令,生成出来的都是错的:(测试摘自Wilson里的test_wilson.rb,注释是我添加的)
  def test_mov_ebx_m_ecx_edx_offset
    # mov  ebx, dword ptr [ecx + edx + 1]
    # 8B 5C 11 01
    asm.ebx.mov(asm.ecx + asm.edx + 1)
    # this test looks broken
    # 8B 1C 51 01   # wrong machine code
    assert_equal [0x8B, 0b00011100, 0b01010001, 1], stream
  end

  def test_mov_ebx_m_ecx_edx_big_offset
    # mov  ebx, dword ptr [ecx + edx + 256]
    # 8B 9C 11 00 01 00 00
    asm.ebx.mov(asm.ecx + asm.edx + 256)
    # this test looks broken as well...
    # 8B 1C 91 00 01 00 00   # wrong machine code
    assert_equal [0x8B, 0b00011100, 0b10010001, 0, 1, 0, 0], stream
  end

  def test_mov_m_ecx_edx_offset_ebx
    # mov  dword ptr [ecx + edx + 1], ebx
    # 89 5C 11 01
    (asm.ecx + asm.edx + 1).mov asm.ebx
    # this test looks broken
    # 89 1C 51 01    # is everything related to ebx broken?
    assert_equal [0x89, 0b00011100, 0b01010001, 1], stream
  end

为什么作者的测试用例里assert的数据就已经跟我所知道的机器码不一致了 OTL
回头我把它修修看看有没有必要发个patch过去……人家或许对Windows没兴趣,顺带就对这patch没兴趣了 =v=

顺带说说上面的例子是如何能运行的。注意到我什么错误检查都没做,所以传入的参数跟我预期的使用方式不符的话程序基本上就要crash了。

第一个例子比较直观,只有最后的dec eax指令可能有点怪。这是因为Ruby的Fixnum表现的是31位带符号整数,普通的31位补码表示的整数要转换为Fixnum的公式是(n << 1) + 1。例子中a与b都是Fixnum,它们的高31位就是原本的数值,而末尾的1则是Fixnum的tag。把它们直接相加,虽然高位是对位加起来了,但末尾的tag本来不该参与运算却也相加了,于是通过dec eax指令把多加的tag再减回去。

后两个例子都涉及到CRuby中对象布局的实现细节。CRuby 1.8.6里有这两个结构体:
struct RBasic {
    unsigned long flags;
    VALUE klass;
};

struct RArray {
    struct RBasic basic;
    long len;
    union {
        long capa;
        VALUE shared;
    } aux;
    VALUE *ptr;
};

数组对象的背后就是靠RArray结构体来支撑的,而它的开头又包含了一个RBasic结构体。可以把RBasic看作是所有Ruby对象都有的header。由此可以知道,在32位x86上,RArray的len成员位于偏移量4*2 == 8的位置上,表示该数组对象的长度(元素个数);ptr成员位于偏移量4*4 == 16的位置上,它所指向的就是数组的实际内容,是一大块连续的空间。

在第二个例子,ary_len里,首先把第一个参数放入eax中。这个参数是指向一个RArray实例的指针。然后再把eax += 8,得到RArray.len的地址。接着通过mov eax, dword ptr [eax]指令,把RArray.len的值取出来。最后使用to_ruby“宏”将数字转换为Ruby的Fixnum,返回。

在第三个例子,ary_store里,过程跟前一个例子差不多,关键思路是达到RARRAY(ary)->ptr[idx] = val的目的。

OK,可以靠Wilson做点更有趣的事情了 ^ ^

你可能感兴趣的:(数据结构,windows,XP,Ruby,rubygems)