Wireshark 实现了一种异常处理机制, 并在解析器等代码中广泛使用, 本文简单分析一下它的实现.

Wireshark 异常处理实现见以下文件:

  • epan/exceptions.h
  • epan/except.h
  • epan/except.c

:::tips 本文所有示例代码见: github.com/zzqcn/wsdev/tree/main/exception :::

1 基本用法

以下代码展示了基本用法.
处理异常:

  1. // epan/packet.c, dissect_record()
  2. TRY {
  3. /* Add this tvbuffer into the data_src list */
  4. add_new_data_source(&edt->pi, edt->tvb, record_type);
  5. /* Even though dissect_frame() catches all the exceptions a
  6. * sub-dissector can throw, dissect_frame() itself may throw
  7. * a ReportedBoundsError in bizarre cases. Thus, we catch the exception
  8. * in this function. */
  9. call_dissector_with_data(frame_handle, edt->tvb, &edt->pi, edt->tree, &frame_dissector_data);
  10. }
  11. CATCH(BoundsError) {
  12. g_assert_not_reached();
  13. }
  14. CATCH2(FragmentBoundsError, ReportedBoundsError) {
  15. proto_tree_add_protocol_format(edt->tree, proto_malformed, edt->tvb, 0, 0,
  16. "[Malformed %s: Packet Length]",
  17. record_type);
  18. }
  19. ENDTRY;

抛出异常:

  1. // epan/dissectors/packet-dns.c, expand_dns_name()
  2. case 0x80:
  3. THROW(ReportedBoundsError);
  4. break;

Wireshark 代码注释已经给出了用法与注意事项:

  1. // epan/exceptions.h
  2. /* Usage:
  3. *
  4. * TRY {
  5. * code;
  6. * }
  7. *
  8. * CATCH(exception) {
  9. * code;
  10. * }
  11. *
  12. * CATCH2(exception1, exception2) {
  13. * code;
  14. * }
  15. *
  16. * CATCH3(exception1, exception2, exception3) {
  17. * code;
  18. * }
  19. *
  20. * CATCH4(exception1, exception2, exception3, exception4) {
  21. * code;
  22. * }
  23. *
  24. * CATCH5(exception1, exception2, exception3, exception4, exception5) {
  25. * code;
  26. * }
  27. *
  28. * CATCH6(exception1, exception2, exception3, exception4, exception5,
  29. *exception6) { code;
  30. * }
  31. *
  32. * CATCH7(exception1, exception2, exception3, exception4, exception5,
  33. *exception6, exception7) { code;
  34. * }
  35. *
  36. * CATCH_NONFATAL_ERRORS {
  37. * code;
  38. * }
  39. *
  40. * CATCH_BOUNDS_ERRORS {
  41. * code;
  42. * }
  43. *
  44. * CATCH_BOUNDS_AND_DISSECTOR_ERRORS {
  45. * code;
  46. * }
  47. *
  48. * CATCH_ALL {
  49. * code;
  50. * }
  51. *
  52. * FINALLY {
  53. * code;
  54. * }
  55. *
  56. * ENDTRY;
  57. *
  58. * ********* Never use 'goto' or 'return' inside the TRY, CATCH*, or
  59. * ********* FINALLY blocks. Execution must proceed through ENDTRY before
  60. * ********* branching out.
  61. *
  62. * All CATCH's must precede a CATCH_ALL.
  63. * FINALLY must occur after any CATCH or CATCH_ALL.
  64. * ENDTRY marks the end of the TRY code.
  65. * TRY and ENDTRY are the mandatory parts of a TRY block.
  66. * CATCH, CATCH_ALL, and FINALLY are all optional (although
  67. * you'll probably use at least one, otherwise why "TRY"?)
  68. *
  69. * GET_MESSAGE returns string ptr to exception message
  70. * when exception is thrown via THROW_MESSAGE()
  71. *
  72. * To throw/raise an exception.
  73. *
  74. * THROW(exception)
  75. * RETHROW rethrow the caught exception
  76. *
  77. * A cleanup callback is a function called in case an exception occurs
  78. * and is not caught. It should be used to free any dynamically-allocated data.
  79. * A pop or call_and_pop should occur at the same statement-nesting level
  80. * as the push.
  81. *
  82. * CLEANUP_CB_PUSH(func, data)
  83. * CLEANUP_CB_POP
  84. * CLEANUP_CB_CALL_AND_POP
  85. */

2 C实现异常处理

C 实现异常处理的基本原理是使用 setjmp/longjmp 函数, 详情参考我的这一篇文章.

3 Wireshark中的实现

Wireshark 在以下文件:

  • epan/exceptions.h
  • epan/except.h

实现了通用异常处理框架, 并在

  • epan/except.c

对此框架进行了进一步封装.

3.1 整体设计

异常处理语句

Wireshark 的一次 TRY/CATCH/FINALLY/ENTRY 语句调用展开后, 原理如下所示:

  1. /*
  2. * {
  3. * caught = FALSE:
  4. * x = setjmp();
  5. * if (x == 0) {
  6. * <TRY code>
  7. * }
  8. * if (!caught && x == 1) {
  9. * caught = TRUE;
  10. * <CATCH(1) code>
  11. * }
  12. * if (!caught && x == 2) {
  13. * caught = TRUE;
  14. * <CATCH(2) code>
  15. * }
  16. * if (!caught && (x == 3 || x == 4)) {
  17. * caught = TRUE;
  18. * <CATCH2(3,4) code>
  19. * }
  20. * if (!caught && (x == 5 || x == 6 || x == 7)) {
  21. * caught = TRUE;
  22. * <CATCH3(5,6,7) code>
  23. * }
  24. * if (!caught && x != 0) {
  25. * caught = TRUE;
  26. * <CATCH_ALL code>
  27. * }
  28. * <FINALLY code>
  29. * if(!caught) {
  30. * RETHROW(x)
  31. * }
  32. * }<ENDTRY tag>
  33. */

要点:

  1. TRY 封装 setjmp, 用于设置跳转点, 它的返回值是异常类型. 还将异常状态(caught)初始化为 0. 直接调用 setjmp 返回值为0, 所以进入 TRY code 执行.
  2. 当 TRY 中的代码抛出异常时, 会调用 longjmp, 并设置异常状态, 此时代码流程跳转到 setjmp 处, 且 caught 不为 0, 此时根据 setjmp 的返回值, 即异常类型, 进入不同的 CATCH 语句.
  3. CATCH 语句在调用用户代码前, 会将异常状态重置为 1.
  4. FINALLY 语句执行一些无论异常是否发生, 都需要做的操作, 比如清理资源. 上面来自 Wireshark 源码的注释里 FINALLY 里有一个 RETHROW, 实际上代码里并不会这么做, RETHROW 一般在 CATCH 语句中.
  5. ENDTRY 结束整个异常处理语句块

上述异常处理流程可用下图表示:
企业微信截图_16315897723580.png

异常栈

Wireshark 异常处理还实现了异常栈, 从而允许 TRY 等语句嵌套调用, 就如同函数调用栈一样. 下图展示了一种带有异常栈的异常处理的简单情况, 看到这里应该还不太理解其中的原理, 但阅读完本文后就懂了.
异常处理.png

其中有内外两份层异常处理代码(即调用 TRY/CATCH/ENDTRY 的代码), 分别捕获并处理了异常 B 与 异常 A; 也有内外内外两层业务代码, 其中对于异常情况分别抛出了异常 A 与 B 两类异常.

  1. 外层代码的 TRY 初始化异常信息 X, 将 id 设为 ANY, 表示处理接受所有异常, 这也是 Wireshark 代码中的实际情况, 最后把 X 放入异常栈
  2. 之后, 外层代码 TRY 语句中的代码又调用到内层异常处理, 引发其中 TRY 的调用, 内层 TRY 调用与外层类似, 最后把异常信息 Y 也放入异常栈. 此时, 栈顶是 Y
  3. 内层代码 TRY 语句中的代码调用到外层业务代码, 遇到异常情况, 它报要抛出异常 A, 其异常代码为 123. 此时它首先遍历异常栈, 匹配第一个接受异常 A 的异常信息, 由于前面两个 TRY 都表示接受所有异常, 所以栈顶 Y 真接命中, 然后外层业务代码会将 Y 的实际异常赋值为异常 B. 最后, 调用 longjmp, 参数是 Y 上的 jmp_buf bufY, 这将导致执行流程跳转到外层代码的 CATCH(A) 处
  4. 内层代码调用 CATCH(A) 中的异常处理代码, 处理异常 A
  5. 内层代码调用 ENDTRY, 完成它自己的异常处理流程. ENTRY 会修改异常栈, 进行一个出栈操作, 把 Y 弹出, 此时异常栈中只剩 X
  6. 此时执行流程回到外层代码 TRY 语句中, 接着调用内层业务代码, 它遇到异常情况, 抛出异常 B, 后续流程与第 3 步一样
  7. 外层异常处理调用 CATCH(B) 中的语句处理异常 B
  8. 外层异常处理调用 ENDTRY, 完成异常处理流程, 此时再进行一次出栈操作, 把 X 弹出, 此时异常栈为空

多线程场景

上述异常处理实现, 使用了全局变量, 如异常栈. 如果要在多线程场景下使用, 还需要完善. Wireshark 使用了 pthread 线程私有数据来实现这一功能. 见 epan/except.c, 如果条件编译宏 KAZLIB_POSIX_THREADS 打开, 则编译多线程支持.

简单来说有以下要点:

  • 异常模块初始化时(except_init), 创建多个 pthread key
  • 各个线程调用异常处理功能, 当需要访问某些共享变量时, 使用 pthread_setspecific/pthread_getspecific 来代替直接访问, 这两个函数的参数需要共享变量对应的, 全局一致的 pthread key

这样, 异常处理语句在读写涉及的变量, 如异常栈时, 就变成了在当前线程中读写私有变量, 与其他线程隔离, 实现了某种多线程安全. 但如果要线程间的异常处理, 即一个线程抛出异常, 但要在另一个线程中捕获并处理, 应该还是没法处理的.

3.2 数据结构

异常 id

  1. typedef struct
  2. {
  3. unsigned long except_group;
  4. unsigned long except_code;
  5. } except_id_t;

异常

  1. typedef struct
  2. {
  3. except_id_t volatile except_id;
  4. const char *volatile except_message;
  5. void *volatile except_dyndata;
  6. } except_t;

异常栈节点

  1. struct except_stacknode
  2. {
  3. struct except_stacknode *except_down;
  4. enum except_stacktype except_type;
  5. union
  6. {
  7. struct except_catch *except_catcher;
  8. struct except_cleanup *except_cleanup;
  9. } except_info;
  10. };

异常栈全局变量, 及入栈与出栈等基本操作:

  1. static struct except_stacknode *stack_top;
  2. #define get_top() (stack_top)
  3. #define set_top(T) (stack_top = (T))
  4. static void stack_push(struct except_stacknode *node)
  5. {
  6. node->except_down = get_top();
  7. set_top(node);
  8. }

其中有两种异常类型, 分别对应两个实际的异常栈处理类型:

  1. enum except_stacktype
  2. {
  3. XCEPT_CLEANUP,
  4. XCEPT_CATCHER
  5. };
  6. struct except_cleanup
  7. {
  8. void (*except_func)(void *);
  9. void *except_context;
  10. };
  11. struct except_catch
  12. {
  13. const except_id_t *except_id;
  14. size_t except_size;
  15. except_t except_obj;
  16. jmp_buf except_jmp;
  17. };

3.3 初始化与反初始化

主要是修改一个初始化计数器, 似乎对异常处理本身没什么影响.

初始化:

  1. int except_init(void)
  2. {
  3. assert(init_counter < INT_MAX);
  4. init_counter++;
  5. return 1;
  6. }

反初始化:

  1. void except_deinit(void)
  2. {
  3. assert(init_counter > 0);
  4. init_counter--;
  5. }

3.4 TRY

异常处理代码调用 TRY 调用功能代码, 开启异常处理流程.

  1. #define TRY \
  2. { \
  3. except_t *volatile exc; \
  4. volatile int except_state = 0; \
  5. static const except_id_t catch_spec[] = { \
  6. {XCEPT_GROUP_WIRESHARK, XCEPT_CODE_ANY}}; \
  7. except_try_push(catch_spec, 1, &exc); \
  8. \
  9. if (except_state & EXCEPT_CAUGHT) \
  10. except_state |= EXCEPT_RETHROWN; \
  11. except_state &= ~EXCEPT_CAUGHT; \
  12. \
  13. if (except_state == 0 && exc == 0) \
  14. /* user's code goes here */

TRY 先初始化了一次异常处理流程所需的数据:

  • 异常指针 exc
  • 异常ID catch_spec
    初始值为 XCEPT_CODE_ANY, 表示要捕获所有异常
  • 异常状态 except_state
    初始值为0, 后续 CATCH/FINALLY 等改变这个值

except_try_push

  1. #define except_try_push(ID, NUM, PPE) \
  2. { \
  3. struct except_stacknode except_sn; \
  4. struct except_catch except_ch; \
  5. except_setup_try(&except_sn, &except_ch, ID, NUM); \
  6. if (setjmp(except_ch.except_jmp)) \
  7. *(PPE) = &except_ch.except_obj; \
  8. else \
  9. *(PPE) = 0

这里做了两件事:

  • 调用 except_setup_try
    把异常初始值放入异常处理栈顶
  • 调用 setjmp
    从 C 实现异常处理的知识我们知道, setjmp 返回 0 表示初次的直接调用, 设定一个跳转点; 而返回非 0 表示从 longjmp 调用返回, 此时把异常指针置为设定 catch 时的异常

except_setup_try

  1. void except_setup_try(struct except_stacknode *esn,
  2. struct except_catch *ech,
  3. const except_id_t id[],
  4. size_t size)
  5. {
  6. ech->except_id = id;
  7. ech->except_size = size;
  8. ech->except_obj.except_dyndata = 0;
  9. esn->except_type = XCEPT_CATCHER;
  10. esn->except_info.except_catcher = ech;
  11. stack_push(esn);
  12. }

这个函数主要是用来设置异常栈节点.

3.5 THROW

功能代码检查到异常情况时, 调用 THROW 抛出异常.

  1. #define THROW(x) except_throw(XCEPT_GROUP_WIRESHARK, (x), NULL)

except_throw:

  1. WS_NORETURN void except_throw(long group, long code, const char *msg)
  2. {
  3. except_t except;
  4. except.except_id.except_group = group;
  5. except.except_id.except_code = code;
  6. except.except_message = msg;
  7. except.except_dyndata = 0;
  8. #ifdef _WIN32
  9. if (code == DissectorError && IsDebuggerPresent()) {
  10. DebugBreak();
  11. }
  12. #endif
  13. do_throw(&except);
  14. }

do_throw

do_throw 实际抛出异常(except_t 类型).

  1. WS_NORETURN static void do_throw(except_t *except)
  2. {
  3. struct except_stacknode *top;
  4. assert(except->except_id.except_group != 0 &&
  5. except->except_id.except_code != 0);
  6. for (top = get_top(); top != 0; top = top->except_down) {
  7. if (top->except_type == XCEPT_CLEANUP) {
  8. top->except_info.except_cleanup->except_func(
  9. top->except_info.except_cleanup->except_context);
  10. } else {
  11. struct except_catch *catcher = top->except_info.except_catcher;
  12. const except_id_t *pi = catcher->except_id;
  13. size_t i;
  14. assert(top->except_type == XCEPT_CATCHER);
  15. except_free(catcher->except_obj.except_dyndata);
  16. for (i = 0; i < catcher->except_size; pi++, i++) {
  17. if (match(&except->except_id, pi)) {
  18. catcher->except_obj = *except;
  19. set_top(top);
  20. longjmp(catcher->except_jmp, 1);
  21. }
  22. }
  23. }
  24. }
  25. set_top(top);
  26. get_catcher()(except); /* unhandled exception */
  27. abort();
  28. }

do_throw 按从栈顶到栈底的顺序遍历整个异常栈, 重点是第 21-24 行, 如果当前要抛出的异常类型, 与之前设定要捕获的某个异常类型一样, 则将之前设定的捕获对象的实际异常置为当前异常, 将这个异常放到栈顶, 并调用 longjmp 跳转到 TRY 语句中的 setjmp 处, 准备捕获并处理这个异常.

如果遍历完整个异常栈, 没有匹配到当前异常的捕获设定, 则说明异常处理代码的 TRY 没有想要捕获这种异常, 最终会执行第 30 行, 调用未捕获异常的处理函数(catcher), 进行未捕获异常的处理. 这个函数是可以设置的.

match:

  1. static int match(const volatile except_id_t *thrown, const except_id_t *caught)
  2. {
  3. int group_match = (caught->except_group == XCEPT_GROUP_ANY ||
  4. caught->except_group == thrown->except_group);
  5. int code_match = (caught->except_code == XCEPT_CODE_ANY ||
  6. caught->except_code == thrown->except_code);
  7. return group_match && code_match;
  8. }

由于 TRY 语句中把 except_code 置为了 XCEPT_CODE_ANY, 所以这个 match 函数永远返回真, 所以所有类型的异常都会被成功抛出, 即永远执行不到 do_throw 函数的第 30 行.

3.6 CATCH

CATCH 捕获具体的异常类型, 并执行异常处理语句.

  1. /* the (except_state |= EXCEPT_CAUGHT) in the below is a way of setting
  2. * except_state before the user's code, without disrupting the user's code if
  3. * it's a one-liner.
  4. */
  5. #define CATCH(x) \
  6. if (except_state == 0 && exc != 0 && exc->except_id.except_code == (x) && \
  7. (except_state |= EXCEPT_CAUGHT)) \
  8. /* user's code goes here */

其中判断条件 exc->except_id.except_code == (x) 决定了只捕获到想要的异常类型. 注意这个 x 并不是 setjmp 的返回值.

3.7 RETHROW

RETHROW 一般在 CATCH 中的异常处理语句内调用, 用来重新抛出已捕获的异常. RETHROW 并不会改变当前异常栈的状态, 只是执行 longjmp, 重新跳转到 setjmp 处, 从而使外层异常处理语句有机会重新处理这个异常.

Wireshark 的 RETHROW 代码有详细注释:

  1. #define RETHROW \
  2. { \
  3. /* check we're in a catch block */ \
  4. g_assert(except_state == EXCEPT_CAUGHT); \
  5. /* we can't use except_rethrow here, as that pops a catch block \
  6. * off the stack, and we don't want to do that, because we want to \
  7. * excecute the FINALLY {} block first. \
  8. * except_throw doesn't provide an interface to rethrow an existing \
  9. * exception; however, longjmping back to except_try_push() has the \
  10. * desired effect. \
  11. * \
  12. * Note also that THROW and RETHROW should provide much the same \
  13. * functionality in terms of which blocks to enter, so any messing \
  14. * about with except_state in here would indicate that THROW is \
  15. * doing the wrong thing. \
  16. */ \
  17. longjmp(except_ch.except_jmp, 1); \
  18. }

3.8 FINALLY

FINALLY 语句用来执行无论异常有没有发生, 都会执行的代码.

  1. #define FINALLY \
  2. if (!(except_state & EXCEPT_FINALLY) && (except_state |= EXCEPT_FINALLY)) \
  3. /* user's code goes here */

3.9 ENDTRY

ENDTRY 结束整个异常处理语句.

  1. #define ENDTRY \
  2. /* rethrow the exception if necessary */ \
  3. if (!(except_state & EXCEPT_CAUGHT) && exc != 0) \
  4. except_rethrow(exc); \
  5. except_try_pop(); \
  6. }

首先是条件判断 !(except_state & EXCEPT_CAUGHT), 由于 CATCH 语句会执行 except_state |= EXCEPT_CAUGHT, 所以只要捕获了某个异常, 那这里这个条件就是假, 只有未捕获时才为真. 未捕获时调用 except_rethow. 最后, 调用 except_try_pop() 修改异常栈, 将当前异常相关栈节点弹出.

except_rethrow

  1. WS_NORETURN void except_rethrow(except_t *except)
  2. {
  3. struct except_stacknode *top = get_top();
  4. assert (top != 0);
  5. assert (top->except_type == XCEPT_CATCHER);
  6. assert (&top->except_info.except_catcher->except_obj == except);
  7. set_top(top->except_down);
  8. do_throw(except);
  9. }

except_rethrow 改变了异常栈, 将栈顶设为当前栈顶的下一项, 并重新异常当前异常, 因为这个异常并没有被任何 CATCH 所捕获. 最后调用 do_throw 抛出异常, 这让外层异常处理代码有机会处理这个未捕获的异常.

except_try_pop

  1. #define except_try_pop() \
  2. except_free(except_ch.except_obj.except_dyndata); \
  3. except_pop(); \
  4. }
  5. struct except_stacknode *except_pop(void)
  6. {
  7. struct except_stacknode *top = get_top();
  8. assert(top->except_type == XCEPT_CLEANUP ||
  9. top->except_type == XCEPT_CATCHER);
  10. set_top(top->except_down);
  11. return top;
  12. }

如果异常被捕获并处理, 最终会调用 except_try_loop, 它进行当前异常相应的清理工作, 并修改异常栈, 将栈顶下移.

3.10 注册清理函数

// TODO

  1. /* Register cleanup functions in case an exception is thrown and not caught.
  2. * From the Kazlib documentation, with modifications for use with the
  3. * Wireshark-specific macros:
  4. *
  5. * CLEANUP_PUSH(func, arg)
  6. *
  7. * The call to CLEANUP_PUSH shall be matched with a call to
  8. * CLEANUP_CALL_AND_POP or CLEANUP_POP which must occur in the same
  9. * statement block at the same level of nesting. This requirement allows
  10. * an implementation to provide a CLEANUP_PUSH macro which opens up a
  11. * statement block and a CLEANUP_POP which closes the statement block.
  12. * The space for the registered pointers can then be efficiently
  13. * allocated from automatic storage.
  14. *
  15. * The CLEANUP_PUSH macro registers a cleanup handler that will be
  16. * called if an exception subsequently occurs before the matching
  17. * CLEANUP_[CALL_AND_]POP is executed, and is not intercepted and
  18. * handled by a try-catch region that is nested between the two.
  19. *
  20. * The first argument to CLEANUP_PUSH is a pointer to the cleanup
  21. * handler, a function that returns nothing and takes a single
  22. * argument of type void*. The second argument is a void* value that
  23. * is registered along with the handler. This value is what is passed
  24. * to the registered handler, should it be called.
  25. *
  26. * Cleanup handlers are called in the reverse order of their nesting:
  27. * inner handlers are called before outer handlers.
  28. *
  29. * The program shall not leave the cleanup region between
  30. * the call to the macro CLEANUP_PUSH and the matching call to
  31. * CLEANUP_[CALL_AND_]POP by means other than throwing an exception,
  32. * or calling CLEANUP_[CALL_AND_]POP.
  33. *
  34. * Within the call to the cleanup handler, it is possible that new
  35. * exceptions may happen. Such exceptions must be handled before the
  36. * cleanup handler terminates. If the call to the cleanup handler is
  37. * terminated by an exception, the behavior is undefined. The exception
  38. * which triggered the cleanup is not yet caught; thus the program
  39. * would be effectively trying to replace an exception with one that
  40. * isn't in a well-defined state.
  41. *
  42. *
  43. * CLEANUP_POP and CLEANUP_CALL_AND_POP
  44. *
  45. * A call to the CLEANUP_POP or CLEANUP_CALL_AND_POP macro shall match
  46. * each call to CLEANUP_PUSH which shall be in the same statement block
  47. * at the same nesting level. It shall match the most recent such a
  48. * call that is not matched by a previous CLEANUP_[CALL_AND_]POP at
  49. * the same level.
  50. *
  51. * These macros causes the registered cleanup handler to be removed. If
  52. * CLEANUP_CALL_AND_POP is called, the cleanup handler is called.
  53. * In that case, the registered context pointer is passed to the cleanup
  54. * handler. If CLEANUP_POP is called, the cleanup handler is not called.
  55. *
  56. * The program shall not leave the region between the call to the
  57. * macro CLEANUP_PUSH and the matching call to CLEANUP_[CALL_AND_]POP
  58. * other than by throwing an exception, or by executing the
  59. * CLEANUP_CALL_AND_POP.
  60. *
  61. */
  1. #define CLEANUP_PUSH(f, a) except_cleanup_push((f), (a))
  2. #define CLEANUP_POP except_cleanup_pop(0)
  3. #define CLEANUP_CALL_AND_POP except_cleanup_pop(1)

参考