Wireshark 实现了一种异常处理机制, 并在解析器等代码中广泛使用, 本文简单分析一下它的实现.
Wireshark 异常处理实现见以下文件:
- epan/exceptions.h
- epan/except.h
- epan/except.c
:::tips 本文所有示例代码见: github.com/zzqcn/wsdev/tree/main/exception :::
1 基本用法
以下代码展示了基本用法.
处理异常:
// epan/packet.c, dissect_record()TRY {/* Add this tvbuffer into the data_src list */add_new_data_source(&edt->pi, edt->tvb, record_type);/* Even though dissect_frame() catches all the exceptions a* sub-dissector can throw, dissect_frame() itself may throw* a ReportedBoundsError in bizarre cases. Thus, we catch the exception* in this function. */call_dissector_with_data(frame_handle, edt->tvb, &edt->pi, edt->tree, &frame_dissector_data);}CATCH(BoundsError) {g_assert_not_reached();}CATCH2(FragmentBoundsError, ReportedBoundsError) {proto_tree_add_protocol_format(edt->tree, proto_malformed, edt->tvb, 0, 0,"[Malformed %s: Packet Length]",record_type);}ENDTRY;
抛出异常:
// epan/dissectors/packet-dns.c, expand_dns_name()case 0x80:THROW(ReportedBoundsError);break;
Wireshark 代码注释已经给出了用法与注意事项:
// epan/exceptions.h/* Usage:** TRY {* code;* }** CATCH(exception) {* code;* }** CATCH2(exception1, exception2) {* code;* }** CATCH3(exception1, exception2, exception3) {* code;* }** CATCH4(exception1, exception2, exception3, exception4) {* code;* }** CATCH5(exception1, exception2, exception3, exception4, exception5) {* code;* }** CATCH6(exception1, exception2, exception3, exception4, exception5,*exception6) { code;* }** CATCH7(exception1, exception2, exception3, exception4, exception5,*exception6, exception7) { code;* }** CATCH_NONFATAL_ERRORS {* code;* }** CATCH_BOUNDS_ERRORS {* code;* }** CATCH_BOUNDS_AND_DISSECTOR_ERRORS {* code;* }** CATCH_ALL {* code;* }** FINALLY {* code;* }** ENDTRY;** ********* Never use 'goto' or 'return' inside the TRY, CATCH*, or* ********* FINALLY blocks. Execution must proceed through ENDTRY before* ********* branching out.** All CATCH's must precede a CATCH_ALL.* FINALLY must occur after any CATCH or CATCH_ALL.* ENDTRY marks the end of the TRY code.* TRY and ENDTRY are the mandatory parts of a TRY block.* CATCH, CATCH_ALL, and FINALLY are all optional (although* you'll probably use at least one, otherwise why "TRY"?)** GET_MESSAGE returns string ptr to exception message* when exception is thrown via THROW_MESSAGE()** To throw/raise an exception.** THROW(exception)* RETHROW rethrow the caught exception** A cleanup callback is a function called in case an exception occurs* and is not caught. It should be used to free any dynamically-allocated data.* A pop or call_and_pop should occur at the same statement-nesting level* as the push.** CLEANUP_CB_PUSH(func, data)* CLEANUP_CB_POP* CLEANUP_CB_CALL_AND_POP*/
2 C实现异常处理
C 实现异常处理的基本原理是使用 setjmp/longjmp 函数, 详情参考我的这一篇文章.
3 Wireshark中的实现
Wireshark 在以下文件:
- epan/exceptions.h
- epan/except.h
实现了通用异常处理框架, 并在
- epan/except.c
3.1 整体设计
异常处理语句
Wireshark 的一次 TRY/CATCH/FINALLY/ENTRY 语句调用展开后, 原理如下所示:
/** {* caught = FALSE:* x = setjmp();* if (x == 0) {* <TRY code>* }* if (!caught && x == 1) {* caught = TRUE;* <CATCH(1) code>* }* if (!caught && x == 2) {* caught = TRUE;* <CATCH(2) code>* }* if (!caught && (x == 3 || x == 4)) {* caught = TRUE;* <CATCH2(3,4) code>* }* if (!caught && (x == 5 || x == 6 || x == 7)) {* caught = TRUE;* <CATCH3(5,6,7) code>* }* if (!caught && x != 0) {* caught = TRUE;* <CATCH_ALL code>* }* <FINALLY code>* if(!caught) {* RETHROW(x)* }* }<ENDTRY tag>*/
要点:
- TRY 封装 setjmp, 用于设置跳转点, 它的返回值是异常类型. 还将异常状态(caught)初始化为 0. 直接调用 setjmp 返回值为0, 所以进入 TRY code 执行.
- 当 TRY 中的代码抛出异常时, 会调用 longjmp, 并设置异常状态, 此时代码流程跳转到 setjmp 处, 且 caught 不为 0, 此时根据 setjmp 的返回值, 即异常类型, 进入不同的 CATCH 语句.
- CATCH 语句在调用用户代码前, 会将异常状态重置为 1.
- FINALLY 语句执行一些无论异常是否发生, 都需要做的操作, 比如清理资源. 上面来自 Wireshark 源码的注释里 FINALLY 里有一个 RETHROW, 实际上代码里并不会这么做, RETHROW 一般在 CATCH 语句中.
- ENDTRY 结束整个异常处理语句块
异常栈
Wireshark 异常处理还实现了异常栈, 从而允许 TRY 等语句嵌套调用, 就如同函数调用栈一样. 下图展示了一种带有异常栈的异常处理的简单情况, 看到这里应该还不太理解其中的原理, 但阅读完本文后就懂了.
其中有内外两份层异常处理代码(即调用 TRY/CATCH/ENDTRY 的代码), 分别捕获并处理了异常 B 与 异常 A; 也有内外内外两层业务代码, 其中对于异常情况分别抛出了异常 A 与 B 两类异常.
- 外层代码的 TRY 初始化异常信息 X, 将 id 设为 ANY, 表示处理接受所有异常, 这也是 Wireshark 代码中的实际情况, 最后把 X 放入异常栈
- 之后, 外层代码 TRY 语句中的代码又调用到内层异常处理, 引发其中 TRY 的调用, 内层 TRY 调用与外层类似, 最后把异常信息 Y 也放入异常栈. 此时, 栈顶是 Y
- 内层代码 TRY 语句中的代码调用到外层业务代码, 遇到异常情况, 它报要抛出异常 A, 其异常代码为 123. 此时它首先遍历异常栈, 匹配第一个接受异常 A 的异常信息, 由于前面两个 TRY 都表示接受所有异常, 所以栈顶 Y 真接命中, 然后外层业务代码会将 Y 的实际异常赋值为异常 B. 最后, 调用 longjmp, 参数是 Y 上的 jmp_buf bufY, 这将导致执行流程跳转到外层代码的 CATCH(A) 处
- 内层代码调用 CATCH(A) 中的异常处理代码, 处理异常 A
- 内层代码调用 ENDTRY, 完成它自己的异常处理流程. ENTRY 会修改异常栈, 进行一个出栈操作, 把 Y 弹出, 此时异常栈中只剩 X
- 此时执行流程回到外层代码 TRY 语句中, 接着调用内层业务代码, 它遇到异常情况, 抛出异常 B, 后续流程与第 3 步一样
- 外层异常处理调用 CATCH(B) 中的语句处理异常 B
- 外层异常处理调用 ENDTRY, 完成异常处理流程, 此时再进行一次出栈操作, 把 X 弹出, 此时异常栈为空
多线程场景
上述异常处理实现, 使用了全局变量, 如异常栈. 如果要在多线程场景下使用, 还需要完善. Wireshark 使用了 pthread 线程私有数据来实现这一功能. 见 epan/except.c, 如果条件编译宏 KAZLIB_POSIX_THREADS 打开, 则编译多线程支持.
简单来说有以下要点:
- 异常模块初始化时(except_init), 创建多个 pthread key
- 各个线程调用异常处理功能, 当需要访问某些共享变量时, 使用 pthread_setspecific/pthread_getspecific 来代替直接访问, 这两个函数的参数需要共享变量对应的, 全局一致的 pthread key
这样, 异常处理语句在读写涉及的变量, 如异常栈时, 就变成了在当前线程中读写私有变量, 与其他线程隔离, 实现了某种多线程安全. 但如果要线程间的异常处理, 即一个线程抛出异常, 但要在另一个线程中捕获并处理, 应该还是没法处理的.
3.2 数据结构
异常 id
typedef struct{unsigned long except_group;unsigned long except_code;} except_id_t;
异常
typedef struct{except_id_t volatile except_id;const char *volatile except_message;void *volatile except_dyndata;} except_t;
异常栈节点
struct except_stacknode{struct except_stacknode *except_down;enum except_stacktype except_type;union{struct except_catch *except_catcher;struct except_cleanup *except_cleanup;} except_info;};
异常栈全局变量, 及入栈与出栈等基本操作:
static struct except_stacknode *stack_top;#define get_top() (stack_top)#define set_top(T) (stack_top = (T))static void stack_push(struct except_stacknode *node){node->except_down = get_top();set_top(node);}
其中有两种异常类型, 分别对应两个实际的异常栈处理类型:
enum except_stacktype{XCEPT_CLEANUP,XCEPT_CATCHER};struct except_cleanup{void (*except_func)(void *);void *except_context;};struct except_catch{const except_id_t *except_id;size_t except_size;except_t except_obj;jmp_buf except_jmp;};
3.3 初始化与反初始化
主要是修改一个初始化计数器, 似乎对异常处理本身没什么影响.
初始化:
int except_init(void){assert(init_counter < INT_MAX);init_counter++;return 1;}
反初始化:
void except_deinit(void){assert(init_counter > 0);init_counter--;}
3.4 TRY
异常处理代码调用 TRY 调用功能代码, 开启异常处理流程.
#define TRY \{ \except_t *volatile exc; \volatile int except_state = 0; \static const except_id_t catch_spec[] = { \{XCEPT_GROUP_WIRESHARK, XCEPT_CODE_ANY}}; \except_try_push(catch_spec, 1, &exc); \\if (except_state & EXCEPT_CAUGHT) \except_state |= EXCEPT_RETHROWN; \except_state &= ~EXCEPT_CAUGHT; \\if (except_state == 0 && exc == 0) \/* user's code goes here */
TRY 先初始化了一次异常处理流程所需的数据:
- 异常指针 exc
- 异常ID catch_spec
初始值为 XCEPT_CODE_ANY, 表示要捕获所有异常 - 异常状态 except_state
初始值为0, 后续 CATCH/FINALLY 等改变这个值
except_try_push
#define except_try_push(ID, NUM, PPE) \{ \struct except_stacknode except_sn; \struct except_catch except_ch; \except_setup_try(&except_sn, &except_ch, ID, NUM); \if (setjmp(except_ch.except_jmp)) \*(PPE) = &except_ch.except_obj; \else \*(PPE) = 0
这里做了两件事:
- 调用
except_setup_try
把异常初始值放入异常处理栈顶 - 调用
setjmp
从 C 实现异常处理的知识我们知道,setjmp返回 0 表示初次的直接调用, 设定一个跳转点; 而返回非 0 表示从longjmp调用返回, 此时把异常指针置为设定 catch 时的异常
except_setup_try
void except_setup_try(struct except_stacknode *esn,struct except_catch *ech,const except_id_t id[],size_t size){ech->except_id = id;ech->except_size = size;ech->except_obj.except_dyndata = 0;esn->except_type = XCEPT_CATCHER;esn->except_info.except_catcher = ech;stack_push(esn);}
这个函数主要是用来设置异常栈节点.
3.5 THROW
功能代码检查到异常情况时, 调用 THROW 抛出异常.
#define THROW(x) except_throw(XCEPT_GROUP_WIRESHARK, (x), NULL)
except_throw:
WS_NORETURN void except_throw(long group, long code, const char *msg){except_t except;except.except_id.except_group = group;except.except_id.except_code = code;except.except_message = msg;except.except_dyndata = 0;#ifdef _WIN32if (code == DissectorError && IsDebuggerPresent()) {DebugBreak();}#endifdo_throw(&except);}
do_throw
do_throw 实际抛出异常(except_t 类型).
WS_NORETURN static void do_throw(except_t *except){struct except_stacknode *top;assert(except->except_id.except_group != 0 &&except->except_id.except_code != 0);for (top = get_top(); top != 0; top = top->except_down) {if (top->except_type == XCEPT_CLEANUP) {top->except_info.except_cleanup->except_func(top->except_info.except_cleanup->except_context);} else {struct except_catch *catcher = top->except_info.except_catcher;const except_id_t *pi = catcher->except_id;size_t i;assert(top->except_type == XCEPT_CATCHER);except_free(catcher->except_obj.except_dyndata);for (i = 0; i < catcher->except_size; pi++, i++) {if (match(&except->except_id, pi)) {catcher->except_obj = *except;set_top(top);longjmp(catcher->except_jmp, 1);}}}}set_top(top);get_catcher()(except); /* unhandled exception */abort();}
do_throw 按从栈顶到栈底的顺序遍历整个异常栈, 重点是第 21-24 行, 如果当前要抛出的异常类型, 与之前设定要捕获的某个异常类型一样, 则将之前设定的捕获对象的实际异常置为当前异常, 将这个异常放到栈顶, 并调用 longjmp 跳转到 TRY 语句中的 setjmp 处, 准备捕获并处理这个异常.
如果遍历完整个异常栈, 没有匹配到当前异常的捕获设定, 则说明异常处理代码的 TRY 没有想要捕获这种异常, 最终会执行第 30 行, 调用未捕获异常的处理函数(catcher), 进行未捕获异常的处理. 这个函数是可以设置的.
match:
static int match(const volatile except_id_t *thrown, const except_id_t *caught){int group_match = (caught->except_group == XCEPT_GROUP_ANY ||caught->except_group == thrown->except_group);int code_match = (caught->except_code == XCEPT_CODE_ANY ||caught->except_code == thrown->except_code);return group_match && code_match;}
由于 TRY 语句中把 except_code 置为了 XCEPT_CODE_ANY, 所以这个 match 函数永远返回真, 所以所有类型的异常都会被成功抛出, 即永远执行不到 do_throw 函数的第 30 行.
3.6 CATCH
CATCH 捕获具体的异常类型, 并执行异常处理语句.
/* the (except_state |= EXCEPT_CAUGHT) in the below is a way of setting* except_state before the user's code, without disrupting the user's code if* it's a one-liner.*/#define CATCH(x) \if (except_state == 0 && exc != 0 && exc->except_id.except_code == (x) && \(except_state |= EXCEPT_CAUGHT)) \/* user's code goes here */
其中判断条件 exc->except_id.except_code == (x) 决定了只捕获到想要的异常类型. 注意这个 x 并不是 setjmp 的返回值.
3.7 RETHROW
RETHROW 一般在 CATCH 中的异常处理语句内调用, 用来重新抛出已捕获的异常. RETHROW 并不会改变当前异常栈的状态, 只是执行 longjmp, 重新跳转到 setjmp 处, 从而使外层异常处理语句有机会重新处理这个异常.
Wireshark 的 RETHROW 代码有详细注释:
#define RETHROW \{ \/* check we're in a catch block */ \g_assert(except_state == EXCEPT_CAUGHT); \/* we can't use except_rethrow here, as that pops a catch block \* off the stack, and we don't want to do that, because we want to \* excecute the FINALLY {} block first. \* except_throw doesn't provide an interface to rethrow an existing \* exception; however, longjmping back to except_try_push() has the \* desired effect. \* \* Note also that THROW and RETHROW should provide much the same \* functionality in terms of which blocks to enter, so any messing \* about with except_state in here would indicate that THROW is \* doing the wrong thing. \*/ \longjmp(except_ch.except_jmp, 1); \}
3.8 FINALLY
FINALLY 语句用来执行无论异常有没有发生, 都会执行的代码.
#define FINALLY \if (!(except_state & EXCEPT_FINALLY) && (except_state |= EXCEPT_FINALLY)) \/* user's code goes here */
3.9 ENDTRY
ENDTRY 结束整个异常处理语句.
#define ENDTRY \/* rethrow the exception if necessary */ \if (!(except_state & EXCEPT_CAUGHT) && exc != 0) \except_rethrow(exc); \except_try_pop(); \}
首先是条件判断 !(except_state & EXCEPT_CAUGHT), 由于 CATCH 语句会执行 except_state |= EXCEPT_CAUGHT, 所以只要捕获了某个异常, 那这里这个条件就是假, 只有未捕获时才为真. 未捕获时调用 except_rethow. 最后, 调用 except_try_pop() 修改异常栈, 将当前异常相关栈节点弹出.
except_rethrow
WS_NORETURN void except_rethrow(except_t *except){struct except_stacknode *top = get_top();assert (top != 0);assert (top->except_type == XCEPT_CATCHER);assert (&top->except_info.except_catcher->except_obj == except);set_top(top->except_down);do_throw(except);}
except_rethrow 改变了异常栈, 将栈顶设为当前栈顶的下一项, 并重新异常当前异常, 因为这个异常并没有被任何 CATCH 所捕获. 最后调用 do_throw 抛出异常, 这让外层异常处理代码有机会处理这个未捕获的异常.
except_try_pop
#define except_try_pop() \except_free(except_ch.except_obj.except_dyndata); \except_pop(); \}struct except_stacknode *except_pop(void){struct except_stacknode *top = get_top();assert(top->except_type == XCEPT_CLEANUP ||top->except_type == XCEPT_CATCHER);set_top(top->except_down);return top;}
如果异常被捕获并处理, 最终会调用 except_try_loop, 它进行当前异常相应的清理工作, 并修改异常栈, 将栈顶下移.
3.10 注册清理函数
// TODO
/* Register cleanup functions in case an exception is thrown and not caught.* From the Kazlib documentation, with modifications for use with the* Wireshark-specific macros:** CLEANUP_PUSH(func, arg)** The call to CLEANUP_PUSH shall be matched with a call to* CLEANUP_CALL_AND_POP or CLEANUP_POP which must occur in the same* statement block at the same level of nesting. This requirement allows* an implementation to provide a CLEANUP_PUSH macro which opens up a* statement block and a CLEANUP_POP which closes the statement block.* The space for the registered pointers can then be efficiently* allocated from automatic storage.** The CLEANUP_PUSH macro registers a cleanup handler that will be* called if an exception subsequently occurs before the matching* CLEANUP_[CALL_AND_]POP is executed, and is not intercepted and* handled by a try-catch region that is nested between the two.** The first argument to CLEANUP_PUSH is a pointer to the cleanup* handler, a function that returns nothing and takes a single* argument of type void*. The second argument is a void* value that* is registered along with the handler. This value is what is passed* to the registered handler, should it be called.** Cleanup handlers are called in the reverse order of their nesting:* inner handlers are called before outer handlers.** The program shall not leave the cleanup region between* the call to the macro CLEANUP_PUSH and the matching call to* CLEANUP_[CALL_AND_]POP by means other than throwing an exception,* or calling CLEANUP_[CALL_AND_]POP.** Within the call to the cleanup handler, it is possible that new* exceptions may happen. Such exceptions must be handled before the* cleanup handler terminates. If the call to the cleanup handler is* terminated by an exception, the behavior is undefined. The exception* which triggered the cleanup is not yet caught; thus the program* would be effectively trying to replace an exception with one that* isn't in a well-defined state.*** CLEANUP_POP and CLEANUP_CALL_AND_POP** A call to the CLEANUP_POP or CLEANUP_CALL_AND_POP macro shall match* each call to CLEANUP_PUSH which shall be in the same statement block* at the same nesting level. It shall match the most recent such a* call that is not matched by a previous CLEANUP_[CALL_AND_]POP at* the same level.** These macros causes the registered cleanup handler to be removed. If* CLEANUP_CALL_AND_POP is called, the cleanup handler is called.* In that case, the registered context pointer is passed to the cleanup* handler. If CLEANUP_POP is called, the cleanup handler is not called.** The program shall not leave the region between the call to the* macro CLEANUP_PUSH and the matching call to CLEANUP_[CALL_AND_]POP* other than by throwing an exception, or by executing the* CLEANUP_CALL_AND_POP.**/
#define CLEANUP_PUSH(f, a) except_cleanup_push((f), (a))#define CLEANUP_POP except_cleanup_pop(0)#define CLEANUP_CALL_AND_POP except_cleanup_pop(1)
