仓颉语言调试工具使用指南

功能简介

cjdb 是一款基于 llvm 后端的 Cangjie 程序命令行调试工具,为 Cangjie 开发者提供程序调试的能力,特性列表如下:

  • 调试器启动被调程序(launch,attach)
  • 源码断点/函数断点/条件断点(breakpoint)
  • 观察点(watchpoint)
  • 程序运行(s,n, finish, continue)
  • 变量查看/变量修改(print,set)
  • 仓颉线程查看(cjthread)

使用说明

调试器加载被调程序(launch,attach)

launch 方式加载被调程序

launch 方式有两种加载方式,如下:

<1> 启动调试器的时候,同时加载被调程序

~/0901/cangjie_test$ cjdb test
(cjdb) target create "test"
Current executable set to '/0901/cangjie-linux-x86_64-release/bin/test' (x86_64).
(cjdb)

<2> 先启动调试器,然后通过 file 命令加载被调程序

~/0901/cangjie_test$ cjdb
(cjdb) file test
Current executable set to '/0901/cangjie/test' (x86_64).
(cjdb)
attach 方式调试被调程序

针对正在运行的程序,支持 attach 方式调试被调程序,如下:

~/0901/cangjie-linux-x86_64-release/bin$ cjdb
(cjdb) attach 15325
Process 15325 stopped
* thread #1, name = 'test', stop reason = signal SIGSTOP
    frame #0: 0x00000000004014cd test`default.main() at test.cj:7:9
   4      var a : Int32 = 12
   5      a = a + 23
   6      while (true) {
-> 7        a = 1
   8      }
   9      a = test(10, 34)
   10     return 1
  thread #2, name = 'FinalProcessor', stop reason = signal SIGSTOP
    frame #0: 0x00007f48c12fc065 libpthread.so.0`__pthread_cond_timedwait at futex-internal.h:205
  thread #3, name = 'PoolGC_1', stop reason = signal SIGSTOP
    frame #0: 0x00007f48c12fbad3 libpthread.so.0`__pthread_cond_wait at futex-internal.h:88
  thread #4, name = 'MainGC', stop reason = signal SIGSTOP
    frame #0: 0x00007f48c12fc065 libpthread.so.0`__pthread_cond_timedwait at futex-internal.h:205
  thread #5, name = 'schmon', stop reason = signal SIGSTOP
    frame #0: 0x00007f48c0fe17a0 libc.so.6`__GI___nanosleep(requested_time=0x00007f48a8ffcb70, remaining=0x0000000000000000) at nanosleep.c:28

Executable module set to "/0901/cangjie-linux-x86_64-release/bin/test".
Architecture set to: x86_64-unknown-linux-gnu.

设置断点

设置源码断点
源码断点设置:breakpoint set --line line_number

例:breakpoint set --line 2

(cjdb) b 2
Breakpoint 1: where = test`default.main() + 13 at test.cj:4:3, address = 0x0000000000401491
(cjdb) b test.cj : 4
Breakpoint 2: where = test`default.main() + 13 at test.cj:4:3, address = 0x0000000000401491
(cjdb)

对于单文件,只需要输入行号即可,对于多文件,需要加上文件名字

--line 指定行号 --file 指定文件

b test.cj:4breakpoint set --file test.cj --line 2的缩写,是 lldb 原生命令,如需了解更多可查看 lldb 相关文档

设置函数断点
函数断点设置:breakpoint set --name function_name

例:breakpoint set --method test

(cjdb) b test
Breakpoint 3: where = test`default.test(int, int) + 19 at test.cj:12:10, address = 0x0000000000401547
(cjdb)

--name 指定要设置函数断点的函数名

设置条件断点
条件断点设置:breakpoint set --file xx.cj --line line_number --condition expression

例:breakpoint set --file test.cj --line 4 --condition 'a==12'

(cjdb) b -f test.cj -l 5 'a==12'
Breakpoint 2: where = test`default.main() + 20 at test.cj:5:7, address = 0x0000000000401498
(cjdb) r
Process 13513 launched: '/0901/cangjie-linux-x86_64-release/bin/test' (x86_64)
Process 13513 stopped
* thread #1, name = 'test', stop reason = breakpoint 2.1
    frame #0: 0x0000000000401498 test`default.main() at test.cj:5:7
   2    main(): Int64 {
   3
   4        var a : Int32 = 12
-> 5        a = a + 23
   6        a = test(10, 34)
   7        return 1
   8    }
(cjdb)

--file 指定文件

--condition 指定条件,支持 ==, !=, >, <, >=, <=, and, or

缩写是 b -f test.cj -l 4 -c 'a==12' ,是 lldb 原生命令

仅支持基础类型变量条件设置(Int8,Int16,Int32,Int64,UInt8,UInt16,UInt32,UInt64,Float32,Float64,Bool,Char)

暂时不支持 Float16 变量类型条件设置

设置观察点

观察点设置:watchpoint set variable -w read variable_name

例:watchpoint set variable -w read a

(cjdb) wa s v -w read a
Watchpoint created: Watchpoint 1: addr = 0x7fffddffed70 size = 8 state = enabled type = r
    declare @ 'test.cj:27'
    watchpoint spec = 'a'
    new value: 10
(cjdb)

b testbreakpoint set --name test的缩写,是 lldb 原生命令

-w 指定观察点点类型,有 read、write、read_write 三种类型

wa s vwatchpoint set variable的缩写,是 lldb 原生命令

只支持在基础类型设置观察点

启动被调程序

执行 r(run)命令

(cjdb) r
Process 2884 launched: '/0901/cangjie-linux-x86_64-release/bin/test' (x86_64)
Process 2884 stopped
* thread #1, name = 'test', stop reason = breakpoint 1.1 2.1
    frame #0: 0x0000000000401491 test`default.main() at test.cj:4:3
   1
   2    main(): Int64 {
   3
-> 4        var a : Int32 = 12
   5        a = a + 23
   6        a = test(10, 34)
   7

可以看到程序停到初始化断点处

执行

单步执行,n(next)
(cjdb) n
Process 2884 stopped
* thread #1, name = 'test', stop reason = step over
    frame #0: 0x0000000000401498 test`default.main() at test.cj:5:7
   2    main(): Int64 {
   3
   4       var a : Int32 = 12
-> 5       a = a + 23
   6       a = test(10, 34)
   7       return 1
   8    }
(cjdb)

从第 4 行运行到第 5 行

执行到下一个断点停止,c(continue)
(cjdb) c
Process 2884 resuming
Process 2884 stopped
* thread #1, name = 'test', stop reason = breakpoint 3.1
    frame #0: 0x0000000000401547 test`default.test(a=10, b=34) at test.cj:12:10
   9
   10   func test(a : Int32, b : Int32) : Int32 {
   11
-> 12     return a + b
   13   }
   14
(cjdb)
函数进入,s
(cjdb) n
Process 5240 stopped
* thread #1, name = 'test', stop reason = step over
    frame #0: 0x00000000004014d8 test`default.main() at test.cj:6:7
   3
   4      var a : Int32 = 12
   5      a = a + 23
-> 6      a = test(10, 34)
   7      return 1
   8    }
   9
(cjdb) s
Process 5240 stopped
* thread #1, name = 'test', stop reason = step in
    frame #0: 0x0000000000401547 test`default.test(a=10, b=34) at test.cj:12:10
   9
   10   func test(a : Int32, b : Int32) : Int32 {
   11
-> 12     return a + b
   13   }
   14
(cjdb)

当遇到函数调用的时候,可通过s命令进入到被调函数的定义声明处

注意,如果被调函数是仓颉标准库函数,则不会进入,若该标准库函数有调用用户函数,同样也不会进入,此时执行该命令等同于n/next命令

函数退出,finish
(cjdb) s
Process 5240 stopped
* thread #1, name = 'test', stop reason = step in
    frame #0: 0x0000000000401547 test`default.test(a=10, b=34) at test.cj:12:10
   9
   10   func test(a : Int32, b : Int32) : Int32 {
   11
-> 12     return a + b
   13   }
   14
(cjdb) finish
Process 5240 stopped
* thread #1, name = 'test', stop reason = step out

Return value: (int) $0 = 44

    frame #0: 0x00000000004014dd test`default.main() at test.cj:6:7
   3
   4      var a : Int32 = 12
   5      a = a + 23
-> 6      a = test(10, 34)
   7      return 1
   8    }
   9
(cjdb)

执行finish命令,退出当前函数,返回到上一个调用栈函数

变量查看

查看局部变量,locals
(cjdb) locals
(Int32) a = 12
(Int64) b = 68
(Int32) c = 13
(Array<Int64>) array = {
  [0] = 2
  [1] = 4
  [2] = 6
}
(pkgs.Rec) newR2 = {
  age = 5
  name = "string"
}
(cjdb)

当调试器停到程序的某个位置时,使用locals可以看到程序当前位置所在函数生命周期范围内,所有的局部变量,只能正确查看当前位置已经初始化的变量,当前未初始化的变量无法正确查看

查看单个变量,print variable_name

例:print b

(cjdb) print b
(Int64) $0 = 110
(cjdb)

使用print命令,后跟要查看具体变量的名字,也可用简写p

查看 String 类型变量
(cjdb) print newR2.name
(String) $0 = "string"
(cjdb)
查看 struct、class 类型变量
(cjdb) print newR2
(pkgs.Rec) $0 = {
  age = 5
  name = "string"
}
(cjdb)
查看数组
(cjdb) print array
(Array<Int64>) $0 = {
  [0] = 2
  [1] = 4
  [2] = 6
  [3] = 8
}
(cjdb) print array[1..3]
(Array<Int64>) $1 = {
  [1] = 4
  [2] = 6
}
(cjdb)

支持查看基础类型(Int8,Int16,Int32,Int64,UInt8,UInt16,UInt32,UInt64,Float16,Float32,Float64,Bool,Unit,Char)

支持范围查看,区间 [start..end) 为左闭右开区间,暂不支持逆序

对于非法区间或对非数组类型查看区间会报错提示

(cjdb) print array
(Array<Int64>) $0 = {
  [0] = 0
  [1] = 1
}
(cjdb) print array[1..3]
error: unsupported expression
(cjdb) print array[0][0]
error: unsupported expression
查看 CString 类型变量
(cjdb) p cstr
(cro.CString) $0 = "abc"
(cjdb) p cstr
(cro.CString) $1 = null
查看全局变量,globals
(cjdb) globals
(Int64) pkgs.Rec.g_age = 100
(Int64) pkgs.g_var = 123
(cjdb)

包名为 default 的全局变量,查看时不显示 default 包名,只显示变量名

用 print 命令查看单个全局变量时,不支持 print+包名+变量名查看全局变量,仅支持 print+变量名进行查看,例如查看全局变量 g_age 应该用如下命令查看

(cjdb) p g_age
(Int64) $0 = 100
(cjdb)

变量修改

(cjdb) set a=30
(Int32) $4 = 30
(cjdb)

可以使用set修改某个局部变量的值,只支持基础数值类型(Int8,Int16,Int32,Int64,UInt8,UInt16,UInt32,UInt64,Float32,Float64)

对于 Bool 类型的变量,可以使用数值 0(false)和非 0(true)进行修改,Char 类型变量,可以使用对应的 ASCII 码进行修改

(cjdb) set b = 0
(Bool) $0 = false
(cjdb) set b = 1
(Bool) $1 = true
(cjdb) set c = 0x41
(Char) $2 = 'A'
(cjdb)

如果修改的值为非数值,或是超出变量的范围,则会报错提示

(cjdb) p c
(Char) $0 = 'A'
(cjdb) set c = 'B'
error: unsupported expression
(cjdb) p b
(Bool) $1 = false
(cjdb) set b = true
error: unsupported expression
(cjdb) p u8
(UInt8) $3 = 123
(cjdb) set u8 = 256
error: unsupported expression
(cjdb) set u8 = -1
error: unsupported expression

仓颉线程查看

支持查看仓颉线程 id 状态以及 frame 信息,暂不支持仓颉线程切换

查看所有仓颉线程
(cjdb) cjthread list
cjthread id: 1, state: running name: cjthread1
    frame #0: 0x000055555557c140 main`ab::main() at varray.cj:16:1
cjthread id: 2, state: pending name: cjthread2
    frame #0: 0x00007ffff7d8b9d5 libcangjie-runtime.so`CJ_CJThreadPark + 117
(cjdb)
查看仓颉线程调用栈

查看指定仓颉线程调用栈

(cjdb) cjthread backtrace 1
cjthread #1 state: pending name: cangjie
  frame #0: 0x00007ffff7d8b9d5 libcangjie-runtime.so`CJ_CJThreadPark + 117
  frame #1: 0x00007ffff7d97252 libcangjie-runtime.so`CJ_TimerSleep + 66
  frame #2: 0x00007ffff7d51b5d libcangjie-runtime.so`CJ_MRT_FuncSleep + 33
  frame #3: 0x0000555555591031 main`std/sync::sleep(std/time::Duration) + 45
  frame #4: 0x0000555555560941 main`default::lambda.0() at complex.cj:9:3
  frame #5: 0x000055555555f68b main`default::std/core::Future<Unit>::execute(this=<unavailable>) at future.cj:124:35
  frame #6: 0x00007ffff7d514f1 libcangjie-runtime.so`___lldb_unnamed_symbol1219 + 7
  frame #7: 0x00007ffff7d4dc52 libcangjie-runtime.so`___lldb_unnamed_symbol1192 + 114
  frame #8: 0x00007ffff7d8b09a libcangjie-runtime.so`CJ_CJThreadEntry + 26
(cjdb)

cjthread backtrace 1 命令中 1 为指定的 cjthread ID

注意事项

  • 进行调试的程序必须已经经过编译的 debug 版本,如使用下述命令编译的程序文件:

    cjc -g test.cj -o test
    

FAQs

  1. docker 环境下 cjdb 报 error: process launch failed: 'A' packet returned an error: 8

    root@xxx:/home/cj/cangjie-example#cjdb ./hello
    (cjdb) target create "./hello"
    Current executable set to '/home/cj/cangjie-example/hello' (x86_64).
    (cjdb) b main
    Breakpoint 1: 2 locations.
    (cjdb) r
    error: process launch failed: 'A' packet returned an error: 8
    (cjdb)
    

    问题原因:docker 创建容器时,未开启 SYS_PTRACE 权限

    解决方案:创建新容器时加上如下选项,并且删除已存在容器

    docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --security-opt apparmor=unconfined
    
  2. cjdb 报 stop reason = signal XXX

    Process 32491 stopped
    * thread #2, name = 'PoolGC_1', stop reason = signal SIGABRT
        frame #0: 0x00007ffff450bfb7 lib.so.6`__GI_raise(sig=2) at raise.c:51
    

    问题原因:程序持续产生 SIG 信号触发调试器暂停

    解决方案:可执行如下命令屏蔽此类信号

    (cjdb) process handle --pass true --stop false --notify true SIGBUS
    NAME         PASS   STOP   NOTIFY
    ===========  =====  =====  ======
    SIGBUS       true   false  true
    (cjdb)
    
  3. 在 spawn 或使用 sleep 语句的场景,执行 next 命令,没有停在下一步

    (cjdb) next
    i number = 1
    Process 22768 stopped
    * thread #1, name = 'cjdb_sleep', stop reason = step over
        frame #0: 0x00000000004015ed cjdb_sleep`default.main() at cjdb_sleep.cj:4:5
       1    main() {
       2        for (i in 0..5) {
       3            print("i number = ${i}\n")
    -> 4            sleep(100 * Duration.millisecond) // sleep for 100ms.
       5        }
       6        var ava :Int8 = 3
       7        return 0
    (cjdb) next
    i number = 2
    i number = 3
    i number = 4
    Process 22768 exited with status = 0 (0x00000000)
    

    问题原因:cjdb 执行 next 默认是单线程模式

    解决方案:使用 next -m all-threads 可减少此问题出现概率;或者在启动调试前设置环境变量cjProcessorNum为 1,这样做会使并发线程数量为 1,协程运行框架只有一个线程,不会涉及线程切换:

    export cjProcessorNum=1
    
  4. 由于仓颉 runtime 中的 GC 使用 SIGSEGV 信号实现,cjdb 在启动时会默认不捕获 SIGSEGV 信号,用户如果需要在调试时捕获此信号,可使用命令重新设置,例如 process handle -p true -s true -n true SIGSEGV 将设置 Pass Stop NOTIFY 动作为 true。

  5. 在调试代码过程中发生异常时, 如果 catch 语句块中未包含断点,那么无法通过 next/s 等调试指令进入 catch 块。其原因在于:仓颉使用 LLVM 的 LandingPad 机制的来实现异常处理, 而该机制无法通过控制流明确 try 语句块中的抛出的异常会由哪一个 catch 语句块捕获,所以无法明确执行的代码。类似问题在 clang++ 中也存在。

  6. 用户定义了一个泛型对象后,调试单步进入该对象的 init 函数时,栈信息显示的函数名称会包含两个包名,一个是实例化该泛型对象所在的包名,另外一个是泛型定义所在的包名。

    * thread #1, name = 'main', stop reason = step in
        frame #0: 0x0000000000404057 main`default.p1.Pair<String, Int64>.init(a="hello", b=0) at a.cj:21:9
       18       let x: T
       19       let y: U
       20       public init(a: T, b: U) {
    -> 21           x = a
       22           y = b
       23       }
    
  7. 对于 Enum 类型的显示, 如果该 Enum 的构造器存在参数的情况下, 会显示成如下样式:

    enum E {
        Ctor(Int64, String) | Ctor
    }
    
    main() {
        var temp = E.Ctor(10, "String")
        0
    }
    
    ========================================
    (cjdb) p temp
    (E) $0 = Ctor {
      arg_1 = 10
      arg_2 = "String"
    }
    

    其中 arg_x 并非是一个可打印的成员变量,Enum 内实际并没有命名为 arg_x 的成员变量。

  8. 仓颉 CJDB 基于 lldb 构建, 所以支持 lldb 原生基础功能,详情见 lldb 官方文档: https://lldb.llvm.org

附录

cjdb 独有命令

命令简写简要描述参数说明
globals查看全局变量无参数
locals查看局部变量无参数
printp查看单个变量参数为变量名称,例 print variable_name
set修改变量参数为表达式,例 set variable_name = value
finish函数退出无参数
cjthread轻量级线程查看无参数