前言
SBCL win32版的官方发布版本,最近几个版本(从1.2.8到最新的1.2.13),一直存在一个很烦人的bug,在控制台界面输入Ctrl+C组合键时,SBCL就会异常退出。在SBCL里面,使用Ctrl+C本来可以中断/停止正在运行的程序。
我从1.2.11版本开始注意到有这个问题,在1.2.11之前我使用的1.2.7版本是正常的。现在发布了1.2.13问题仍然没有解决。Google了一下,原来也有人发现了这个问题,并且和SBCL的开发人员有过交流,不过没有找到问题原因。他们的讨论可以参考这个帖子。
这位老兄也是厉害,他根据代码提交记录,一个版本一个版本的尝试,发现了其中一个commit之后就出了问题。但是开发人员站出来说,这次提交基本没有功能变化,只是整理了一下代码,把sb-saftepoint和sb-thread的feature判断换了一部分(类似于C里面的条件编译),但是在现在的版本中这两个feature是同时打开的,所以也不应有问题。我也去查看了这次提交的代码情况,的确找不到有什么疑点。
这一段时间我自己尝试分析这个bug,断断续续折腾了好几天,终于发现了问题所在。最后的原因比较简单,但是过程曲折,由于一开始的方法不对,花了很多时间在一些SBCL实现方面的原理的学习上。如果是对于普通的一个应用程序,应该分析起来没那么困难,但是SBCL用gdb、visual studio等常用的调试器分析都不方便。为了阅读方便,我先直接描述问题原因和解决办法,再将相关的背景知识和调试经验简单介绍一下。
问题原因简单的说就是:GCC优化不当。
问题现象
打开sbcl,按Ctrl+C,即出现经典的windows程序异常通知窗口。如果电脑上安装有调试器比如VS20XX,那么会提示是否立即进行调试。
其实SBCL没有就此死去,如果你把异常报告框拖到一边,SBCL仍然能继续使用,但是不能在对Ctrl-C进行响应了。
这说明SBCL主线程还运行正常!
原因
问题代码在SBCL的runtime模块,代码位置src/runtime/thread.c
简化起见,引用源代码时我会删除不想关的部分,感兴趣的读者可以直接查看sbcl的源码。
void callback_wrapper_trampoline( lispobj arg0, lispobj arg1, lispobj arg2)
{
struct thread* th = arch_os_get_current_thread();
if (!th) { /* callback invoked in non-lisp thread */
init_thread_data scribble;
attach_os_thread(&scribble);
funcall3(StaticSymbolFunction(ENTER_FOREIGN_CALLBACK), arg0,arg1,arg2);
detach_os_thread(&scribble);
return;
}
}
请看detach_os_thread
和 callback_wrapper_trampoline
,这两个函数的开头第一句是相同的,都是获取thread结构指针。而这两个函数在同一个文件(thread.c),这给gcc提供了一个优化的机会,他认为在detach_os_thread的时候,th
还没有变化,所以把detach_os_thread
inline到callback_wrapper_trampoline
中了,并且把detach_os_thread
的第一条语句去掉了。
void detach_os_thread(init_thread_data *scribble)
{
struct thread *th = arch_os_get_current_thread();</span>
undo_init_new_thread(th, scribble);
odxprint(misc, "deattach_os_thread: detached");
pthread_setspecific(lisp_thread, (void *)0);
thread_sigmask(SIG_SETMASK, &scribble->oldset, 0);
}
其实,th
在if
判断之前是NULL
,但是在attach_os_thread
里面会赋有效值。在detach_os_thread
中根据新的th
做一些清理的工作. 现在既然获取新的th的语句被优化掉了,那么到了undo_init_new_thread的时候,就变成了访问非法指针了, 异常就这么出现了.
static void
undo_init_new_thread(struct thread *th, init_thread_data *scribble)
{
int lock_ret;
//...
gc_alloc_update_page_tables(BOXED_PAGE_FLAG, &th->alloc_region);
当然我们还要看看到底th
是怎么获取的(thread.h):
static inline struct thread* arch_os_get_current_thread()
__attribute__((__const__));
static inline struct thread *arch_os_get_current_thread(void)
{
register struct thread *me=0;
__asm__ ("movl %%fs:0xE10+(4*63), %0" : "=r"(me) :);
return me;
}
这个函数定义还是略有可疑的,他不仅声明为inline
,还声明为__attribute__((__const__))
,这表示这个函数结果只依赖参数或者全局变量。如此是不是就可以大胆优化了?是不是这个定义有问题?
但是这个还不够,最终确认还是得依靠汇编代码,只有汇编代码才不骗人。callback_wrapper_trampoline
的反汇编如下,可以看出在执行funcll3和undo_init_new_thread
之间并没有重新获取th
.
Dump of assembler code for function callback_wrapper_trampoline:
0x00416e20 <+0>: push %ebp
0x00416e21 <+1>: mov %esp,%ebp
0x00416e23 <+3>: push %edi
0x00416e24 <+4>: push %esi
0x00416e25 <+5>: push %ebx
0x00416e26 <+6>: sub $0x1c,%esp
0x00416e29 <+9>: mov 0x10(%ebp),%ebx
0x00416e2c <+12>: call 0x41cb40 <pthread_np_notice_thread>
0x00416e31 <+17>: mov %fs:0xf0c,%eax ;; 读取th
0x00416e37 <+23>: test %eax,%eax ;; 判断th是不是0,如果是0跳转
0x00416e39 <+25>: je 0x416ed1 <callback_wrapper_trampoline+177>
;;
0x00416ed1 <+177>: lea -0x18(%ebp),%esi ;;跳转到这里
0x00416ed4 <+180>: mov %esi,(%esp)
0x00416ed7 <+183>: call 0x416b50 <attach_os_thread> ;;执行attach_os_thread
0x00416edc <+188>: mov 0xc(%ebp),%eax
0x00416edf <+191>: mov %ebx,0xc(%esp)
0x00416ee3 <+195>: mov %eax,0x8(%esp)
0x00416ee7 <+199>: mov 0x8(%ebp),%eax
0x00416eea <+202>: mov %eax,0x4(%esp)
0x00416eee <+206>: mov 0x22100664,%eax
0x00416ef3 <+211>: and $0xfffffff8,%eax
0x00416ef6 <+214>: mov 0x8(%eax),%eax
0x00416ef9 <+217>: mov %eax,(%esp)
0x00416efc <+220>: call 0x403440 <funcall3> ;;执行funcall3
0x00416f01 <+225>: mov 0x4420d4,%ebx
0x00416f07 <+231>: test %ebx,%ebx
0x00416f09 <+233>: jne 0x416f50 <callback_wrapper_trampoline+304>
0x00416f0b <+235>: xor %eax,%eax
0x00416f0d <+237>: mov %esi,%edx
0x00416f0f <+239>: call 0x4166d0 <undo_init_new_thread> ;;执行undo_init_new_thread
0x00416f14 <+244>: mov 0x4420d4,%ecx
0x00416f1a <+250>: test %ecx,%ecx
0x00416f1c <+252>: je 0x416f2a <callback_wrapper_trampoline+266>
0x00416f1e <+254>: movl $0x43ec7e,(%esp)
0x00416f25 <+261>: call 0x40bfe0 <odxprint_fun>
0x00416f2a <+266>: call 0x41c7a0 <pthread_self>
0x00416f2f <+271>: mov 0x442104,%ebx
0x00416f35 <+277>: mov -0x18(%ebp),%edx
0x00416f38 <+280>: movl $0x0,0x33c(%eax,%ebx,4)
0x00416f43 <+291>: mov %edx,0x14(%eax)
0x00416f46 <+294>: jmp 0x416e73 <callback_wrapper_trampoline+83>
0x00416f4b <+299>: nop
0x00416f4c <+300>: lea 0x0(%esi,%eiz,1),%esi
0x00416f50 <+304>: movl $0x43ec61,(%esp)
0x00416f57 <+311>: call 0x40bfe0 <odxprint_fun>
0x00416f5c <+316>: jmp 0x416f0b <callback_wrapper_trampoline+235>
如何解决
我尝试了一些方法,例如声明变量为volatile,修改attribute等,都没有效果,只有在嵌入asm中加入volatile有效。
__asm__ volatile ("movl %%fs:0xE10+(4*63), %0" : "=r"(me) :);
如果你有更好的解决思路,欢迎探讨。
背景知识
Ctrl+C是如何处理的
Ctrl+C其实是系统产生了一个信号SIGINT,发送给应用程序. SBCL的信号处理部分在不同的平台有不同的实现,win32的实现入口在src/code/warm-mswin.lisp. 其实现方式大致如下:
- 通过win32系统调用SetConsoleCtrlHandler将控制台信号处理接管。简单说就是由应用程序提供一个回调函数注册到系统,系统在产生了信号的时候进行回调。下面这段lisp代码是sbcl定义的外部C函数接口,定义之后就拥有了一个lisp函数set-console-ctrl-handler,可以直接使用。
(define-alien-routine ("SetConsoleCtrlHandler" set-console-ctrl-handler) int
(callback (function (:stdcall int) int))
(enable int))
- SetConsoleCtrlHandler的第一参数是回调函数,自然SBCL需要提供一个符合cstdcall调用规范的函数地址。由于SBCL lisp的调用规范和C语言函数调用规范不同,所以这部分是一个很复杂的过程。在SBCL术语中这个叫alien-callback。关于alien-callback网上资料非常少,只在sbcl internal文档有简单的几行描述。sbcl定义的console control handler的回调函数如下:
(sb-alien::define-alien-callback *alien-console-control-handler* (:stdcall int)
((event-code int))
(if (ignore-errors (funcall *console-control-handler* event-code)) 1 0))
其中*console-control-handler*
是一个全局变量,存储具体的处理函数。主要是中断正在运行的线程,给一些调试提示等。
3.
alien-callback的实现
定义一个alien-callback的方式如下:
(sb-alien::define-alien-callback *alien-console-control-handler* (:stdcall int)
((event-code int))
(if (ignore-errors (funcall *console-control-handler* event-code)) 1 0))
define-alien-callback是一个宏,上面的代码展开如下:
* (pprint (macroexpand ‘(sb-alien::define-alien-callback *alien-console-control-handler* (:stdcall int)
((event-code int))
(if (ignore-errors (funcall *console-control-handler* event-code)) 1 0))))
(PROGN
(DEFUN *ALIEN-CONSOLE-CONTROL-HANDLER* (EVENT-CODE)
(IF (IGNORE-ERRORS (FUNCALL *CONSOLE-CONTROL-HANDLER* EVENT-CODE))
1
0))
(DEFPARAMETER *ALIEN-CONSOLE-CONTROL-HANDLER*
(SB-ALIEN-INTERNALS:ALIEN-CALLBACK (FUNCTION (:STDCALL INT) INT)
#‘*ALIEN-CONSOLE-CONTROL-HANDLER*)))
它定义了一个函数,内容为callback函数的实现部分,再定义了一个同名的全局变量, 这个才是真正传给SetConsoleCtrlHandler的函数指针。SB-ALIEN-INTERNALS:ALIEN-CALLBACK的实现颇为复杂,他对回调函数和定义进行注册记录下来,并生产一段满足c调用规范的代码,建立lisp执行环境最后调用目标lisp函数。
满足c调用的汇编代码在src/compiler/x86/c-call.lisp中,alien-callback-assembler-wrapper
,它生成一段汇编代码,建立了c调用堆栈,然后调用了一个c函数callback_wrapper_trampoline
(没错,出错的就是这个函数). 这个函数生成sbcl lisp的线程环境,让回调的lisp代码在这个线程环境中执行,然后生成sbcl lisp的调用栈环境。这部分代码就像是c world和lisp world的边界,他理解两个世界的运行规则,在中间建立起时空隧道。
void callback_wrapper_trampoline( lispobj arg0, lispobj arg1, lispobj arg2) <pre name="code" class="cpp"> struct thread* th = arch_os_get_current_thread();
if (!th) { /* callback invoked in non-lisp thread */
init_thread_data scribble;
attach_os_thread(&scribble);
funcall3(StaticSymbolFunction(ENTER_FOREIGN_CALLBACK), arg0,arg1,arg2);
detach_os_thread(&scribble);
return;
}
}
lispobj funcall3(lispobj function, lispobj arg0, lispobj arg1, lispobj arg2)
{
lispobj args[3];
args[0] = arg0;
args[1] = arg1;
args[2] = arg2;
return safe_call_into_lisp(function, args, 3);
}
static inline lispobj safe_call_into_lisp(lispobj fun, lispobj *args, int nargs)
{
#ifndef LISP_FEATURE_SB_SAFEPOINT
/* SIG_STOP_FOR_GC needs to be enabled before we can call lisp:
* otherwise two threads racing here may deadlock: the other will
* wait on the GC lock, and the other cannot stop the first
* one... */
check_gc_signals_unblocked_or_lose(0);
#endif
return call_into_lisp(fun, args, nargs);
}
有关指针th
th是sbcl存储其线程结构的地方,为了避免和系统冲突,存在在线程TLS(thread local storage)的最后一个位置(63)。这其实算是一个hack,系统是不知道sbcl使用了这个存储位置的。关于tls的说明,可以参考wikipedia。
win32系统中,TLS位置为TIB(Thread Information Block)的0xE10偏移处。寄存器FS专门存放TIB,了解之后,就明白下面代码中这行嵌入汇编的含义了。
static inline struct thread *arch_os_get_current_thread(void)
{
register struct thread *me=0;
__asm__ ("movl %%fs:0xE10+(4*63), %0" : "=r"(me) :);
return me;
}
其实获取TLS的value,windows有专门的函数TlsGetValue
我们可以用gdb反汇编查看TlsGetValue的实现,和上面的汇编有一点不一样他是先根据FS获取偏移0x18处的值得到TIB的线性地址首地址,然后在偏移E10得到TLS的。这和wikipedia说的一样:
The TIB of the current thread can be accessed as an offset of segment register FS (x86) or GS (x64).
It is not common to access the TIB fields by an offset from FS:[0], but rather first getting a linear self-referencing pointer to it stored at FS:[0x18]. That pointer can be used with pointer arithmetics or be cast to a struct pointer.
意思是使用FS直接偏移也是可以的,但是很少这样用。sbcl偏就这么用 :)
Bug定位过程
首先查看windows给出的错误报告,提示访问的非法地址为0x42623c
上述address为异常时代码位置,用gdb反汇编sbcl.exe可以找到这个地址是函数gc_alloc_update_page_tables。
(gdb) disassemble 0x42623c
Dump of assembler code for function gc_alloc_update_page_tables:
0x00426230 <+0>: push %ebp
0x00426231 <+1>: mov %esp,%ebp
0x00426233 <+3>: push %edi
0x00426234 <+4>: push %esi
0x00426235 <+5>: push %ebx
0x00426236 <+6>: sub $0x2c,%esp
0x00426239 <+9>: mov 0xc(%ebp),%eax
0x0042623c <+12>: mov 0x8(%eax),%eax
0x0042623f <+15>: test %eax,%eax
0x00426241 <+17>: mov %eax,-0x20(%ebp)
0x00426244 <+20>: jne 0x426253 <gc_alloc_update_page_tables+35>
0x00426246 <+22>: mov 0xc(%ebp),%eax
0x00426249 <+25>: cmpl $0xffffffff,0xc(%eax)
0x0042624d <+29>: je 0x42642c <gc_alloc_update_page_tables+508>
0x00426253 <+35>: mov -0x20(%ebp),%eax
0x00426256 <+38>: lea 0x1(%eax),%edi
0x00426259 <+41>: mov 0x4385c0,%eax
0x0042625e <+46>: cmp $0x445b00,%eax
0x00426263 <+51>: je 0x426635 <gc_alloc_update_page_tables+1029>
0x00426269 <+57>: cmp $0xffffffff,%eax
0x0042626c <+60>: je 0x4265d7 <gc_alloc_update_page_tables+935>
对应的c代码如下
void
gc_alloc_update_page_tables(int page_type_flag, struct alloc_region *alloc_region)
{
boolean more;
page_index_t first_page;
page_index_t next_page;
os_vm_size_t bytes_used;
os_vm_size_t region_size;
os_vm_size_t byte_cnt;
page_bytes_t orig_first_page_bytes_used;
int ret;
first_page = alloc_region->first_page;
/* Catch an unused alloc_region. */
if ((first_page == 0) && (alloc_region->last_page == -1))
return;
next_page = first_page+1;
ret = thread_mutex_lock(&free_pages_lock);
gc_assert(ret == 0);
对比可以知道在访问alloc_region->first_page
时发生异常。
按道理应该可以通过gdb attach到这个应用程序,查看调用栈的,但是从错误报告看居然存在6个线程,实际上sbcl只有一个主线程,算上处理Ctrl+C信号的系统调用,最多也只有两个线程,可能线程结构坏了,或者sbcl使用了自定义的线程,gdb无法识别了。这样无法使用gdb挂载到线程查看调用栈,实际情况也是如此,每个线程的调用栈都不是发生异常的情况。
无法使用gdb的情况,我们可以添加打印代码。也可以在lisp的回调函数中添加(break),当lisp中断时使用lisp的调试工具查看调用链。这里我们先选择第2种方法。在sbcl中执行下面的代码:
#<PACKAGE "SB-WIN32">
*
(set-console-ctrl-handler *alien-console-control-handler* 0) ;; 去注册原有的Ctrl+C处理函数。
1
*
(sb-alien::define-alien-callback *alien-console-control-handler* (:stdcall int) ;;定义一个新的处理函数,并添加(break)
((event-code int))
(format t "naive console handler~%")
(break) ;; break
1)
; in: SB-ALIEN::DEFINE-ALIEN-CALLBACK *ALIEN-CONSOLE-CONTROL-HANDLER*
; (SB-INT:NAMED-LAMBDA SB-WIN32::*ALIEN-CONSOLE-CONTROL-HANDLER*
; (SB-WIN32::EVENT-CODE)
; (BLOCK SB-WIN32::*ALIEN-CONSOLE-CONTROL-HANDLER*
; (FORMAT T "naive console handler~%")
; (BREAK)
; 1))
;
; caught STYLE-WARNING:
; The variable EVENT-CODE is defined but never used. ;; 有个warning,没有使用参数event-code,这里我们先不管它
;
; compilation unit finished
; caught 1 STYLE-WARNING condition
STYLE-WARNING: redefining SB-WIN32::*ALIEN-CONSOLE-CONTROL-HANDLER* in DEFUN
*ALIEN-CONSOLE-CONTROL-HANDLER*
*
(set-console-ctrl-handler *alien-console-control-handler* 1) ;;重新注册我们的处理函数
1
;;;;;这里输入Ctrl+C
* naive console handler ;; 自定义的函数被调用了,打印消息。而且执行到(break),停了下来。
debugger invoked on a SIMPLE-CONDITION in thread
#<FOREIGN-THREAD "foreign callback" RUNNING {25984031}>:
break
(sb-thread:release-foreground) ;;;;; 切换到停下来的线程
0Resuming thread #<FOREIGN-THREAD "foreign callback" RUNNING {25984031}>
Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.
restarts (invokable by number or by possibly-abbreviated name):
0: [CONTINUE] Return from BREAK.
1: [ABORT ] abort thread
(#<FOREIGN-THREAD "foreign callback" RUNNING {25984031}>)
(SB-WIN32::*ALIEN-CONSOLE-CONTROL-HANDLER* #<unavailable argument>)
0] backtrace ;;;;;;打印调用栈
Backtrace for: #<SB-THREAD:FOREIGN-THREAD "foreign callback" RUNNING {25984031}>
0: (SB-WIN32::*ALIEN-CONSOLE-CONTROL-HANDLER* #<unavailable argument>)
1: ((LAMBDA (SB-ALIEN::ARGS-POINTER SB-ALIEN::RESULT-POINTER FUNCTION) :IN "c:/sbcl-32/src/code/warm-mswin.fasl") 4767690 4767686 #<FUNCTION SB-WIN32::*ALIEN-CONSOLE-CONTROL-HANDLER*>)
2: ((FLET #:WITHOUT-INTERRUPTS-BODY-1216 :IN SB-THREAD::INITIAL-THREAD-FUNCTION-TRAMPOLINE))
3: ((FLET SB-THREAD::WITH-MUTEX-THUNK :IN SB-THREAD::INITIAL-THREAD-FUNCTION-TRAMPOLINE))
4: ((FLET #:WITHOUT-INTERRUPTS-BODY-584 :IN SB-THREAD::CALL-WITH-MUTEX))
5: (SB-THREAD::CALL-WITH-MUTEX #<CLOSURE (FLET SB-THREAD::WITH-MUTEX-THUNK :IN SB-THREAD::INITIAL-THREAD-FUNCTION-TRAMPOLINE) {122FD45}> #<SB-THREAD:MUTEX "thread result lock" owner: #<SB-THREAD:FOREIGN-THREAD "foreign callback" RUNNING {25984031}>> NIL T NIL)
6: (SB-THREAD::INITIAL-THREAD-FUNCTION-TRAMPOLINE #<SB-THREAD:FOREIGN-THREAD "foreign callback" RUNNING {25984031}> NIL #<FUNCTION SB-ALIEN-INTERNALS:ENTER-ALIEN-CALLBACK> NIL T 1 4767690 4767686)
7: ("foreign function: #x42C160")
8: ("foreign function: #x403472")
9: ("foreign function: #x416F01")
10: ("foreign function: #x22100A1E")
0]
最后4个是c调用栈(其实最后一个就是lisp动态生成的alien-callback-assembler-wrapper),lisp的调试器不能直接解析c的调用栈。我们用gdb手工解析如下:
(gdb) disassemble 0x42C160,+1
Dump of assembler code from 0x42c160 to 0x42c16a:
0x0042c160 <call_into_lisp+144>: jae 0x42c164 <call_into_lisp+148>
(gdb) disassemble 0x403472,+1
Dump of assembler code from 0x403472 to 0x403473:
0x00403472 <funcall3+50>: leave
(gdb) disassemble 0x416F01,+1
Dump of assembler code from 0x416f01 to 0x416f02:
0x00416f01 <callback_wrapper_trampoline+225>: mov 0x4420d4,%ebx
End of assembler dump.
所以调用栈如下:
call_info_lisp
funcall3
callback_wrapper_trampoline
查看这几个函数的实现,知道是在callback_wrapper_trampoline
中调用了出问题的gc_alloc_update_page_tables
调用关系如下:
void callback_wrapper_trampoline( lispobj arg0, lispobj arg1, lispobj arg2)
{
struct thread* th = arch_os_get_current_thread();
if (!th) { /* callback invoked in non-lisp thread */
init_thread_data scribble;
attach_os_thread(&scribble);
funcall3(StaticSymbolFunction(ENTER_FOREIGN_CALLBACK), arg0,arg1,arg2);
detach_os_thread(&scribble);
return;
}
}
void
detach_os_thread(init_thread_data *scribble)
{
struct thread *th = arch_os_get_current_thread();
undo_init_new_thread(th, scribble);
odxprint(misc, "deattach_os_thread: detached");
pthread_setspecific(lisp_thread, (void *)0);
thread_sigmask(SIG_SETMASK, &scribble->oldset, 0);
}
static void
undo_init_new_thread(struct thread *th, init_thread_data *scribble)
{
int lock_ret;
/* Kludge: Changed the order of some steps between the safepoint/
* non-safepoint versions of this code. Can we unify this more?
*/
#ifdef LISP_FEATURE_SB_SAFEPOINT
block_blockable_signals(0, 0);
gc_alloc_update_page_tables(BOXED_PAGE_FLAG, &th->alloc_region);
到这里基本可以确定是指针th异常。为了验证,在detach_os_thread中添加一行打印,打印出th值。重新编译sbcl.exe
一运行果不其然,th=0!
到这一步,后面再结合汇编,问题就容易定位了。
Bug到底是怎么引入的
为什么以前的版本没有这个问题呢
原来在那次commit中,callback_wrapper_trampoline
这个函数的实现从safepoint.c
被移到了thread.c
,和attach_os_thread
, detach_os_thread
放到了一起。以前他们不在一个文件,没法优化, callback_wrapper_trampoline
老老实实的调用detach_os_thread
,自然就会重新获取th
。
移动了一个函数的位置,gcc就帮忙整出了这个问题!想想也真是冤。
几个调试技巧
- 如果改动了sbcl的runtime,需要重新编译,那么可以直接执行 make -C src/runtime, 可以实现增量编译,比起调用make.sh,或者make-target-1.sh快多了
- 如果改动了lisp实现部分,如果是make-target-2的部分,即非交叉编译的部分的代码,改动不大的话,比如加加打印什么的,根本不用重新编译,将对应的代码(函数定义等)copy出来,修改修改,在sbcl的repl中执行一下即可,记得先(in-package …), sbcl编译过程中用了一些特殊的package name,和read-macro,需要作相应的修改,比如sb!alien改成sb-alien,#!+win32改成#+win32. 记住sbcl本身大部分也是用lisp写的,完全可能使用lisp本身的各种方便的功能。
- sbcl使用第三方调试工具是比较困难的,比如gdb和visual studio,但是碰到像本文这个bug的时候不得不用,两者用起来都不顺利,不过可以结合起来,visual studio可以控制执行、反汇编、单步等,这些比gdb好,但是反汇编的代码没有符号表,看起来不方便,而这块刚好可以用gdb弥补,我就是这样结合使用才定位出这个bug来的。
版权声明:本文为博主原创文章,未经博主允许不得转载。