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 _WIN32
if (code == DissectorError && IsDebuggerPresent()) {
DebugBreak();
}
#endif
do_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)