论坛首页 编程语言技术论坛

从main.c开始走进Ruby-登上调试Ruby之旅

浏览 10612 次
该帖已经被评为良好帖
作者 正文
   发表时间:2010-08-18   最后修改:2010-08-19

我想更深入的了解Ruby内部的实现,出发点或许过于天真,

 

我想了解下这门语言的实现,从中或许可以学习到某些思路,
比如:

  • 如果我们要设计另外一种动态语言该如何去下手,
  • 如何将其他语言的特性融合进Ruby或者我们要设计的语言,
  • 特定领域的特定语言该如何设计(不要一门又广又全的语言,但又不是DSL)。


题目是《从main.c开始走进Ruby》,那我们需要以下的准备工作

  1. Ruby的源码,我采用的是ruby-1.9.2,
  2. gdb(神器)
  3. 一个IDE,eclipse(可选)

得到源码之后,首先要对其进行编译,不能采用默认的编译方式,

要在执行./configure的时候加上如下参数

 

  • CFLAGS="-ggdb -g3 -gdwarf-2"

或者在Makefile中添加如下标志参数:

  • optflags = -O
  • debugflags = -ggdb -g3 -gdwarf-2

这样编译的Ruby包含了充足的调试信息,同时还保留了Ruby源码中大量使用的宏信息,

比如我们在gdb中显示如下的宏:

(gdb) info macro RTEST
Defined at ./include/ruby/ruby.h:349
  included at /home/zheng.cuizh/ruby-1.9.2-rc2/ruby.c:18
#define RTEST(v) (((VALUE)(v) & ~Qnil) != 0)
 

如果编译参数没有添加,那么编译出来的代码是没办法看到macro信息的。

 

在编译好Ruby后,需要使用make install安装,安装好后就可以登上调试Ruby之旅了。

 

调试Ruby有几种方法,只要能在你知道的地方下个断点,并且Ruby解析器能够执行到该断点即可,

我想到以下两种方式:

  1. ruby -e "h=Hash.new"
  2. irb

对于第一种方式,我们直接使用gdb ruby启动即可,然后在gdb的console中设置属性及断点:

  • gdb ruby
  • set args -e "h=Hash.new"
  • b main

对于第二种方式,我们先执行irb,然后再开启一个终端,通过ps ax|grep irb|grep -v grep|awk '{print $1}' 找到irb的进程id,然后用gdb attach到该进程即可:

  • irb
  • gdb - <irb_pid>
  • b rb_hash_initialize
  • c
  • h=Hash.new#irb中输入

这样的话就会在初始化Hash对象的时候被断到,然后就可以继续调试了。

 

个人推荐采用第二种方式来调试Ruby解析器。

 

 

这里我要多说一下,第一种方式的最后一步b main的意思是对main()这个位于main.c中的入口函数。

 

(gdb) list main
18
19      RUBY_GLOBAL_SETUP
20
21      int
22      main(int argc, char **argv)
23      {
24      #省略
30
31          ruby_sysinit(&argc, &argv);
32          {
33              RUBY_INIT_STACK;
34              ruby_init();
35              return ruby_run_node(ruby_options(argc, argv));
36          }
37      }
(gdb)
Line number 38 out of range; main.c has 37 lines.

 

Ruby是一种解释型语言,它的代码都是没编译过的字符串,

  • 在Ruby解析器执行起来后,

vm_exec(th);

  • 通过读入Ruby代码文件,动态解析

    PREPARE_PARSE_MAIN({
        tree = load_file(parser, opt->script, 1, opt);
    });

从而完成边解析边执行的过程。第一种方法的main.c函数就是ruby解析器的入口,当我们在那里下断点后,必定能将Ruby断下来。

在main.c35行这句话:return ruby_run_node(ruby_options(argc, argv));

我们需要在gdb里面通过键入s跟进两次,ruby_run_node和ruby_options这两个函数都需要步入,他们都很重要。其中ruby_run_node会调用vm_exec来执行语法树。

 

 

至此,我们的准备工作就完成了。很简单的步骤,却很重要。

 

之后的工作就是不断的跟踪Ruby各种类型的实现机制,简单的描述就是在irb里面不停尝试各种类型的new操作,同时在gdb里面不断的对不同类型rb_object_initialize下断点,然后步入每一个想知道的方法中,对某些变量设置观察点,打印出结构体,再步进。

 

真感谢Ruby有个irb这么好用的工具,也是由于动态语言的缘故,代码可以通过eval来执行。

有了irb,我们就可以交互式调试没段代码了。

 

 

 

 

这是走进Ruby的开始,我也刚刚做好准备工作,很多Ruby内部的实现我还没有读懂,我边读边写,不过我要在我读懂了之后再写出来,由于悟性的问题,我可能写的很慢或者有错误的地方,而且思路可能还会很跳跃^-^,希望高手能指点一下。

 


Ruby有好多预定以变量,多的让我不可能记住,这里有个网页,记录了和Ruby有关的一些关键字:
http://www.tutorialspoint.com/ruby/ruby_predefined_constants.htm

我们在使用Ruby的argv参数数组时,知道$0是当前进程的程序名称,
比如我们在启动的irb中执行$0,就会返回"irb",
这个$0是在这里被保存的:

rb_argv0 = rb_str_new4(rb_progname);



rb_progname是什么,debug跟踪进去可以看到如下定义:

#define rb_progname (GET_VM()->progname)
#define GET_VM() ruby_current_vm



这俩段宏可以得到当前Ruby解析器的名字,这个名字是保存在下面这个结构体中的,

下面这个是ruby vm_core.h中的解析器结构体,

typedef struct rb_vm_struct {
    VALUE self;

    rb_thread_lock_t global_vm_lock;

    struct rb_thread_struct *main_thread;
    struct rb_thread_struct *running_thread;

    st_table *living_threads;
    VALUE thgroup_default;

    int running;
    int thread_abort_on_exception;
    unsigned long trace_flag;
    volatile int sleeper;

    /* object management */
    VALUE mark_object_ary;

    VALUE special_exceptions[ruby_special_error_count];

    /* load */
    VALUE top_self;
    VALUE load_path;
    VALUE loaded_features;
    struct st_table *loading_table;

    /* signal */
    struct {
    VALUE cmd;
    int safe;
    } trap_list[RUBY_NSIG];

    /* hook */
    rb_event_hook_t *event_hooks;

    int src_encoding_index;

    VALUE verbose, debug, progname;
    VALUE coverages;

    struct unlinked_method_entry_list_entry *unlinked_method_entry_list;

#if defined(ENABLE_VM_OBJSPACE) && ENABLE_VM_OBJSPACE
    struct rb_objspace *objspace;
#endif
} rb_vm_t;




和python等其他脚本语言类似,脚本语言无法真实的实现native thread来做并行运算,可以通过下面这个成员看出端倪:

全局解释器锁

rb_thread_lock_t global_vm_lock;

 

在调试过程中要善用bt这个命令,通过bt我们可以看到某个函数的调用栈,配合eclipse等IDE(没有IDE的话就用layout查看代码),可以很好的分析出代码的走向,一个实例创建的过程。

 

比如在以irb方式调试的时候,刚刚断入irb的进程,执行bt后看如下输出:

 

(gdb) bt
#0  0x000000304360d5cb in read () from /lib64/libpthread.so.0
#1  0x0000000000527803 in rb_thread_blocking_region (func=0x42b0e0 <internal_read_func>, data1=0x7fff42460860, ubf=0x527960 <ubf_select>, data2=0xb58bf0) at thread.c:1117
#2  0x000000000042bae8 in io_fillbuf (fptr=0xe88a40) at io.c:587
#3  0x0000000000437004 in rb_io_getbyte (io=<value optimized out>) at io.c:3101
#4  0x0000000000517ee5 in vm_call0 (th=0xb58bf0, recv=15222400, id=1928, argc=0, argv=0x0, me=0xc28600) at vm_eval.c:78
#5  0x000000000051b27b in rb_funcall (recv=15222400, mid=0, n=0) at vm_eval.c:234
#6  0x00002aaaaaeb29bd in readline_getc (input=0x3042d516a0) at readline.c:120
#7  0x0000003043e25eba in rl_read_key () from /usr/lib64/libreadline.so.5
#8  0x0000003043e14921 in readline_internal_char () from /usr/lib64/libreadline.so.5
#9  0x0000003043e14d65 in readline () from /usr/lib64/libreadline.so.5
#10 0x000000000041773d in rb_protect (proc=0x2aaaaaeb2940 <readline_get>, data=15701216, state=0x7fff42460e6c) at eval.c:718
#11 0x00002aaaaaeb28ae in readline_readline (argc=2, argv=<value optimized out>, self=<value optimized out>) at readline.c:255
#12 0x000000000050cc95 in vm_call_method (th=0xb58bf0, cfp=0x2af8e53e16c8, num=2, blockptr=0x1, flag=8, id=0, me=0xe217b0, recv=15222560) at vm_insnhelper.c:401
#13 0x00000000005103b7 in vm_exec_core (th=0xb58bf0, initial=<value optimized out>) at insns.def:1006
#14 0x0000000000516c99 in vm_exec (th=0x0) at vm.c:1145
#15 0x000000000051c3c8 in invoke_block_from_c (th=0xb58bf0, block=0xe9d9c0, self=15225000, argc=0, argv=0x0, blockptr=0x0, cref=0x0) at vm.c:557
#16 0x000000000051c77f in rb_vm_invoke_proc (th=0xb58bf0, proc=0xe9d9c0, self=15225000, argc=0, argv=0x2af8e52e2250, blockptr=0x0) at vm.c:603
#17 0x000000000050cc95 in vm_call_method (th=0xb58bf0, cfp=0x2af8e53e18d8, num=0, blockptr=0x1, flag=0, id=0, me=0xc52000, recv=15307040) at vm_insnhelper.c:401
#18 0x00000000005103b7 in vm_exec_core (th=0xb58bf0, initial=<value optimized out>) at insns.def:1006
#19 0x0000000000516c99 in vm_exec (th=0x0) at vm.c:1145
#20 0x000000000051c3c8 in invoke_block_from_c (th=0xb58bf0, block=0x2af8e53e1c18, self=15213640, argc=0, argv=0x0, blockptr=0x0, cref=0x0) at vm.c:557
#21 0x000000000051c888 in loop_i () at vm.c:587
#22 0x00000000004179a9 in rb_rescue2 (b_proc=0x51c850 <loop_i>, data1=0, r_proc=0, data2=0) at eval.c:646
#23 0x00000000005092e9 in rb_f_loop (self=15213640) at vm_eval.c:816
#24 0x000000000050cc95 in vm_call_method (th=0xb58bf0, cfp=0x2af8e53e1bf0, num=0, blockptr=0x2af8e53e1c19, flag=8, id=0, me=0xbe21a0, recv=15213640) at vm_insnhelper.c:401
#25 0x00000000005103b7 in vm_exec_core (th=0xb58bf0, initial=<value optimized out>) at insns.def:1006
#26 0x0000000000516c99 in vm_exec (th=0x0) at vm.c:1145
#27 0x000000000051c3c8 in invoke_block_from_c (th=0xb58bf0, block=0x2af8e53e1d20, self=15213640, argc=1, argv=0x0, blockptr=0x0, cref=0x0) at vm.c:557
#28 0x000000000051c82e in catch_i (tag=4080910, data=<value optimized out>) at vm.c:587
#29 0x00000000005084fd in rb_catch_obj (tag=4080910, func=0x51c7f0 <catch_i>, data=0) at vm_eval.c:1522
#30 0x0000000000509198 in rb_f_catch (argc=<value optimized out>, argv=<value optimized out>) at vm_eval.c:1498
#31 0x000000000050cc95 in vm_call_method (th=0xb58bf0, cfp=0x2af8e53e1cf8, num=1, blockptr=0x2af8e53e1d21, flag=8, id=0, me=0xbe1e20, recv=15213640) at vm_insnhelper.c:401
#32 0x00000000005103b7 in vm_exec_core (th=0xb58bf0, initial=<value optimized out>) at insns.def:1006
#33 0x0000000000516c99 in vm_exec (th=0x0) at vm.c:1145
#34 0x000000000051c3c8 in invoke_block_from_c (th=0xb58bf0, block=0x2af8e53e1ed8, self=12129600, argc=1, argv=0x0, blockptr=0x0, cref=0x0) at vm.c:557
#35 0x000000000051c82e in catch_i (tag=3247374, data=<value optimized out>) at vm.c:587
#36 0x00000000005084fd in rb_catch_obj (tag=3247374, func=0x51c7f0 <catch_i>, data=0) at vm_eval.c:1522
#37 0x0000000000509198 in rb_f_catch (argc=<value optimized out>, argv=<value optimized out>) at vm_eval.c:1498
---Type <return> to continue, or q <return> to quit---
#38 0x000000000050cc95 in vm_call_method (th=0xb58bf0, cfp=0x2af8e53e1eb0, num=1, blockptr=0x2af8e53e1ed9, flag=8, id=0, me=0xbe1e20, recv=12129600) at vm_insnhelper.c:401
#39 0x00000000005103b7 in vm_exec_core (th=0xb58bf0, initial=<value optimized out>) at insns.def:1006
#40 0x0000000000516c99 in vm_exec (th=0x0) at vm.c:1145
#41 0x0000000000516f74 in rb_iseq_eval_main (iseqval=12097560) at vm.c:1386
#42 0x0000000000417bcf in ruby_exec_internal (n=0xb89818) at eval.c:214
#43 0x000000000041a396 in ruby_run_node (n=<value optimized out>) at eval.c:261
#44 0x0000000000416e5d in main (argc=2, argv=0x7fff424632c8) at main.c:35

 

第一行read函数就是irb和使用者交互的方法,

最后一行的main就是Ruby解析器的入口函数,

从下到上就是Ruby解析器整个执行过程。

不过我还没弄明白loop_i这个函数的作用。

 

先写到这里,下一章我也不知道会写什么,因为前提是我看懂了什么^-^

   发表时间:2010-08-18  
赞,最好能写成一个系列吧。读ruby hacking guide半途而废,1.9的代码比1.8应该有比较大的变动吧。
0 请登录后投票
   发表时间:2010-08-18  
強烈建議來個系列...
0 请登录后投票
   发表时间:2010-08-18  
dennis_zane 写道
赞,最好能写成一个系列吧。读ruby hacking guide半途而废,1.9的代码比1.8应该有比较大的变动吧。


fireflyman 写道
強烈建議來個系列...


我也很希望能持续的写下去,但途中肯定会遇到一些问题的,到时候我会把问题抛出来,还请大家不要吝啬各自的才华帮我看看问题,借助大家的帮忙估计还是能写几篇的,希望大家不吝赐教啊!
0 请登录后投票
   发表时间:2010-08-18  
拜 gdb 熟练工 ……
0 请登录后投票
   发表时间:2010-08-18  
嗯我只是想说解释器和解析器是不一样的……
加油,这系列有潜力~
光一个new能看到的东西就已经相当多了,包括GC检查剩余可用空间/触发收集等也在流程中:
static void *
vm_xmalloc(rb_objspace_t *objspace, size_t size)
{
    void *mem;

    if ((ssize_t)size < 0) {
        negative_size_allocation_error("negative allocation size (or too big)");
    }
    if (size == 0) size = 1;

#if CALC_EXACT_MALLOC_SIZE
    size += sizeof(size_t);
#endif

    if ((ruby_gc_stress && !ruby_disable_gc_stress) ||
        (malloc_increase+size) > malloc_limit) {
        garbage_collect_with_gvl(objspace);
    }
    mem = malloc(size);                           // 尝试分配空间
    if (!mem) {                                   // 如果没分配到空间的话,
        if (garbage_collect_with_gvl(objspace)) { // 就触发一次收集
            mem = malloc(size);                   // 收集过后再试一次分配空间
        }
        if (!mem) {
            ruby_memerror();
        }
    }
    malloc_increase += size;

#if CALC_EXACT_MALLOC_SIZE
    objspace->malloc_params.allocated_size += size;
    objspace->malloc_params.allocations++;
    ((size_t *)mem)[0] = size;
    mem = (size_t *)mem + 1;
#endif

    return mem;
}
0 请登录后投票
   发表时间:2010-08-18  
用windbg配合pdb文件可能更妙一些
0 请登录后投票
   发表时间:2010-08-19  
想法和做法都很好,学习
0 请登录后投票
   发表时间:2010-08-19  
通过调试学习真是个不错的方法。同时也能想到困难重重
0 请登录后投票
   发表时间:2010-08-19  
在windbg里最妙的是可以查看源代码:

0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics