|
5 | 5 |
|
6 | 6 | 反射,是指程序检查、修改其自身执行某些方面的能力。动态语言(如 Lua)自然而然支持多种反射特性: |
7 | 7 |
|
8 | | -- 环境特性允许了运行时的全局变量检查; |
9 | | -- 函数(如 `type` 和 `pairs`)允许了运行时的未知数据结构检查和遍历; |
10 | | -- 函数(如 `load` 和 `require`)允许了程序为自身添加代码或更新自己的代码。 |
| 8 | +- 环境特性允许运行时的全局变量检查; |
| 9 | +- 诸如 `type` 及 `pairs` 等函数,允许运行时的未知数据结构检查和遍历; |
| 10 | +- 而诸如 `load` 及 `require` 等函数,则允许程序为自身添加代码,或更新自己的代码。 |
11 | 11 |
|
12 | 12 |
|
13 | | -然而,仍有许多不足之处:程序无法自省其局部变量,程序无法跟踪其执行情况,函数无法获悉其调用者,等等。调试库,the debug library,填补了这些空白。 |
| 13 | +然而,仍缺少许多东西: |
14 | 14 |
|
| 15 | +- 程序无法自省其局部变量; |
| 16 | +- 程序无法跟踪其执行情况; |
| 17 | +- 函数无法获悉其调用者等等。 |
15 | 18 |
|
16 | | -调试库包含两类函数:*内省函数,introspective functions* 和 *钩子,hooks*。内省函数允许我们检查正在运行程序的多个方面,譬如活动函数堆栈、当前执行行及局部变量的值和名称等。钩子允许我们跟踪程序的执行。 |
17 | 19 |
|
| 20 | +调试库,the `debug` library,填补了这些空白。 |
18 | 21 |
|
19 | | -尽管名为调试库,但他并未提供一个 Lua 调试器。不过,他提供了编写我们自己调试器所需的,复杂程度各不相同的全部原语。 |
20 | 22 |
|
| 23 | +调试库包含两类函数: |
21 | 24 |
|
22 | | -与其他库不同,我们应该谨慎使用调试库,use the debug library with parsimony。首先,调试库的某些功能,并不以性能著称。其次,他打破了该门语言的一些神圣真理,比如我们不能从局部变量的词法范围之外,访问该局部变量这一条。虽然调试库与标准库一样直接可用,但我(作者)更倾向于,在任何用到调试库的代码块中,显式地导入他。 |
| 25 | +- *内省函数,introspective functions*; |
| 26 | +- 和 *钩子,hooks*。 |
23 | 27 |
|
24 | 28 |
|
| 29 | +内省函数允许我们检查正在运行程序的多个方面,譬如活动函数堆栈、当前执行行及局部变量的值和名称等。钩子则允许我们跟踪程序的执行。 |
| 30 | + |
| 31 | + |
| 32 | +尽管名为调试库,但他并未提供给我们一个 Lua 调试器。不过,他提供了编写我们自己调试器所需的、复杂程度各不相同的全部原语。 |
| 33 | + |
| 34 | + |
| 35 | +与其他库不同,我们应谨慎使用调试库,use the debug library with parsimony。首先,调试库的某些功能,并不以性能著称。其次,他打破了该门语言的一些神圣真理,比如我们不能从局部变量的词法范围外,访问该局部变量这一条。虽然调试库与标准库一样直接可用,但我(作者)更倾向于,在任何用到调试库的代码块中,显式地导入他。 |
| 36 | + |
25 | 37 |
|
26 | 38 | ## 自省设施 |
27 | 39 |
|
28 | 40 | **Introspective Facilities** |
29 | 41 |
|
30 | 42 |
|
31 | | -调试库中的主要自省函数,是 `getinfo`。其第一个参数可以是某个函数,也可以是某个堆栈层级,a stack level。当我们对函数 `foo` 调用 `debug.getinfo(foo)` 时,他会返回一个包含有关该函数一些数据的表。该表可以包含以下字段: |
| 43 | +调试库中的主要自省函数为 `getinfo`。其第一个参数可以是某个函数,或某个堆栈层级,a stack level。当我们对某个函数 `foo`,调用 `debug.getinfo(foo)` 时,他会返回一个包含有着该函数一些数据的表。该表可能包含以下字段: |
32 | 44 |
|
33 | 45 |
|
34 | | -- `source`:该字段给出函数于何处定义。如果函数是在字符串中定义的(经由一个 `load` 调用),那么 `source` 就是那个字符串。如果函数是在某个文件中定义的,那么 `source` 就是以 `@` 符号为前缀的文件名; |
| 46 | +- `source`:该字段给出函数于何处定义。如果该函数定义在某个字符串中(经由一次 `load` 调用),`source` 就是那个字符串。如果函数定义在在某个文件中,`source` 就是以 `@` 符号为前缀的文件名; |
35 | 47 |
|
36 | | -- `short_src`:该字段是 `source` 简短版本(最多 60 个字符)。这对于错误信息非常有用; |
| 48 | +- `short_src`:该字段是 `source` 简短版本(最多 60 个字符)。这对于错误消息非常有用; |
37 | 49 |
|
38 | | -- `linedefined`:该字段给出了定义函数的源代码第一行编号; |
| 50 | +- `linedefined`:该字段给出来源中定义该函数处的第一行编号; |
39 | 51 |
|
40 | | -- `lastlinedefined`:该字段给出了定义函数的源代码最后一行编号; |
| 52 | +- `lastlinedefined`:该字段给出来源中定义该函数处的最后一行编号; |
41 | 53 |
|
42 | | -- `what`:该字段给出了此函数是什么。如果 `foo` 是个常规 Lua 函数,则选项为 `Lua`;如果是个 C 函数,则选项为 `C`;如果是 Lua 代码块的主要部分,则选项为 `main`; |
| 54 | +- `what`:该字段给出了该函数是什么。如果 `foo` 是个常规 Lua 函数,则选项为 `Lua`;如果是个 C 函数,则选项为 `C`;如果该函数是某个 Lua 代码块的主要部分,则为 `main`; |
43 | 55 |
|
44 | | -- `name`:该字段给到函数的合理名称,例如存储该函数的全局变量名称; |
| 56 | +- `name`:该字段给出一个该函数的合理名称,比如存储该函数的全局变量名字; |
45 | 57 |
|
46 | | -- `namewhat`:该字段给出前一字段的含义。该字段可以是 `global`、`local`、`method`、`field` 或 `''`(空字符串)。空字符串表示 Lua 没有找到函数名称; |
| 58 | +- `namewhat`:该字段给出前一字段的含义。该字段可以是 `global`、`local`、`method`、`field` 或 `''`(空字符串)。空字符串表示 Lua 没有找到该函数的名字; |
47 | 59 |
|
48 | 60 | - `nups`:这是该函数的上值个数,the number of upvalues; |
49 | 61 |
|
50 | 62 | - `nparams`:这是该函数的参数个数; |
51 | 63 |
|
52 | | -- `isvararg`:这表明函数是否为可变参数,whether the function is variadic(一个布尔值); |
| 64 | +- `isvararg`:这表示该函数是否为可变参数(一个布尔值),whether the function is variadic; |
53 | 65 |
|
54 | | -- `activelines`:该字段是个表示函数活动行集合的表。所谓 *活动行,active line*,是指有代码的行,而不是空行或仅包含注释的行。(该信息的一个典型用途,是设置断点。大多数调试器不允许我们在活动行之外设置断点,因为这样的断点是无法到达的。) |
| 66 | +- `activelines`:该字段是个表示函数活动行的集合的表。所谓 *活动行,active line*,是指有代码的行,而不是空行或仅包含注释的行。(此信息的一个典型用途是设置断点。大多数调试器都不允许我们在活动行之外设置断点,因为这样的断点是无法到达的。) |
55 | 67 |
|
56 | 68 | - `func`:该字段为函数本身。 |
57 | 69 |
|
58 | 70 |
|
59 | 71 | 当 `foo` 是个 C 语言函数时,Lua 就没有太多关于他的数据。对于此类函数,只有 `what`、`name`、`namewhat`、`nups` 和 `func` 字段是有意义的。 |
60 | 72 |
|
61 | | -当我们对某个数字 `n` 调用 `debug.getinfo(n)` 时,我们会获取到有关活动于该堆栈级别函数的数据,data about the function active at that stack level。所谓 *堆栈级别*,是个表示当时处于活动状态特定函数的数字。调用 `getinfo` 的函数级别为一,调用他的函数级别为二,依此类推。 (在级别零处,我们会获取到有关 `getinfo` 本身(一个 C 函数)的数据。)如果 `n` 大于堆栈上活动函数的数量,则 `debug.getinfo` 返回 `nil`。当我们通过调用带有堆栈级别的 `debug.getinfo` ,查询某个活动函数时,结果表有两个额外的字段:`currentline`,该函数当时所在的行; `istailcall`(布尔值),如果该函数是通过尾调用调用的,则为 `true`。 (在这种情况下,该函数的真正调用者不再在堆栈上。) |
| 73 | +当我们以某个数字 `n` 调用 `debug.getinfo(n)` 时,我们会获取到有关活动于该堆栈级别函数的数据,data about the function active at that stack level。所谓 *堆栈级别*,是个指向活动于该时刻的某个特定函数的数字。调用 `getinfo` 的函数级别为一,调用他的函数级别为二,依此类推。(在级别零处,我们就会得到有关 `getinfo` 本身(一个 C 函数)的数据。)如果 `n` 大于了堆栈上的活动函数数量,`debug.getinfo` 就会返回 `nil`。当我们通过调用带有堆栈级别的 `debug.getinfo` ,查询某个活动函数时,结果表会有两个额外的字段: |
| 74 | + |
| 75 | +- `currentline`,该函数当时所在的行; |
| 76 | +- `istailcall`(一个布尔值),在该函数是经由一个尾调用而被调用到时,则为 `true`。(在这种情况下,该函数的真正调用者已不在堆栈上了。) |
62 | 77 |
|
63 | | -`name` 字段很棘手。请记住,由于函数在 Lua 中属于头等值,functions are first-class values in Lua,因此函数可能没有名字,也可能有多个名字。Lua 尝试通过查看调用函数的代码,了解某个函数是如何被调用的,来找到该函数的名字。这种方法只有在我们调用带有数字的 `getinfo` ,即在我们请求有关某个特定调用的信息时,才会起作用。 |
| 78 | +`name` 字段很棘手。请记住,由于函数是 Lua 中的头等值,functions are first-class values in Lua,因此函数可能没有名字,也可能有多个名字。Lua 通过查看调用函数代码,了解某个函数是如何被调用的,尝试找到该函数的名字。这种方式只有在我们以某个数字调用 `getinfo` ,即在我们请求有关某个特定调用的信息时,才会起作用。 |
64 | 79 |
|
65 | | -函数 `getinfo` 并不高效。Lua 以不影响程序执行的形式,保存调试信息;高效检索是次要目标。为获得更好性能,`getinfo` 有个可选的第二参数,用于选择要获取的信息。这样,该函数就不会浪费时间,收集用户不需要的数据。该参数的格式是个字符串,每个字母代表一组字段,如下表所示。 |
| 80 | +函数 `getinfo` 并不高效。Lua 以不影响程序执行的形式,保存调试信息;高效检索是次要目标。为达到更好性能,`getinfo` 有个用于选取要获得信息的可选的第二个参数。这样,该函数就不会浪费时间收集用户不需要的数据。该参数的格式是个字符串,其中每个字母会选取一组字段,如下表所示。 |
66 | 81 |
|
67 | 82 | | 选项 | 意义 | |
68 | 83 | | :-: | :- | |
69 | | -| `n` | 选择 `name` 与 `namewhat` | |
70 | | -| `f` | 选择 `func` | |
71 | | -| `S` | 选择 `source`、`short_src`、`what`、`linedefined` 与 `lastlinedefined` | |
72 | | -| `l` | 选择 `currentline` | |
73 | | -| `L` | 选择 `activelines` | |
74 | | -| `u` | 选择 `nups`、`nparams` 与 `isvararg` | |
| 84 | +| `n` | 选取 `name` 与 `namewhat` | |
| 85 | +| `f` | 选取 `func` | |
| 86 | +| `S` | 选取 `source`、`short_src`、`what`、`linedefined` 与 `lastlinedefined` | |
| 87 | +| `l` | 选取 `currentline` | |
| 88 | +| `L` | 选取 `activelines` | |
| 89 | +| `u` | 选取 `nups`、`nparams` 与 `isvararg` | |
75 | 90 |
|
76 | 91 |
|
77 | | -以下函数通过打印活动堆栈的原始回溯,说明了 `debug.getinfo` 用法: |
| 92 | +下面这个函数通过打印出活动堆栈的原始回溯,说明了 `debug.getinfo` 用法: |
78 | 93 |
|
79 | 94 |
|
80 | 95 | ```lua |
81 | 96 | function traceback () |
82 | 97 | for level = 1, math.huge do |
83 | 98 | local info = debug.getinfo(level, "Sl") |
| 99 | + |
84 | 100 | if not info then break end |
| 101 | + |
85 | 102 | if info.what == "C" then -- 是个 C 函数? |
86 | 103 | print(string.format("%d\tC 函数", level)) |
87 | 104 | else -- 是个 Lua 函数 |
88 | 105 | print(string.format("%d\t[%s]:%d", level, info.short_src, info.currentline)) |
89 | 106 | end |
| 107 | + |
90 | 108 | end |
91 | 109 | end |
92 | 110 | ``` |
93 | 111 |
|
94 | | -要改进这个函数并不难,只要包含更多 `getinfo` 中的数据即可。实际上,调试库就提供了这样一个改进版本,即函数 `traceback`。与我们的版本不同,`debug.traceback` 并不打印结果,而是返回一个包含回溯信息的字符串(可能很长): |
| 112 | +要改进这个函数并不难,只要包含更多 `getinfo` 中的数据即可。实际上,调试库就提供了这样一个改进版本,即函数 `traceback`。与我们的版本不同,`debug.traceback` 并不打印结果;而是返回一个包含回溯信息的字符串(可能很长): |
95 | 113 |
|
96 | 114 |
|
97 | 115 | ```console |
@@ -385,12 +403,31 @@ for i = 1, 36 do s = s .. s end |
385 | 403 |
|
386 | 404 | <a name="f-25.5"></a> **控制内存使用** |
387 | 405 | ```lua |
388 | | -{{#include ../scripts/improved_sandbox.lua:3:22}} |
| 406 | +{{#include ../scripts/reflection/improved_sandbox.lua:3:22}} |
389 | 407 |
|
390 | 408 | -- 如前 |
391 | 409 | ``` |
392 | 410 |
|
393 | 411 |
|
394 | | -(End) |
| 412 | +由于在如此少的指令下,内存就能快速增长,我们应该设置一个非常低的限制,或者以小的步骤调用钩子。更具体地说,某个程序可以在 40 条指令内,将某个字符串的大小增加一千倍。因此,我们要么以比每 40 步更高的频率调用钩子,要么将内存限制设为我们真正能承受的千分之一。我(作者)可能会两者兼顾。 |
| 413 | + |
| 414 | +更微妙的问题便是 Lua 的字符串库。我们可在某个字符串上,以方法方式调用这个库中的任何函数。因此,即使这些函数不在环境中,我们也可以调用他们;字面的字符串会将他们,偷偷地带入我们的沙箱。字符串库中的任何函数,都不会影响外部世界,但他们会绕过我们的步骤计数器。(对 C 函数的一次调用,会算作 Lua 中的一条指令。)字符串库中的某些函数,可能是非常危险的 DoS 攻击。例如,在一个步骤中调用 `(“x”):rep(2^30)` 一次,就会吞噬 1 GB 的内存。再举个例子,在我(作者)的新机器上,运行下面的调用 Lua 5.2 需要 13 分钟: |
| 415 | + |
| 416 | + |
| 417 | +```lua |
| 418 | +s = "01234567890123456789012345678901234567890123456789" |
| 419 | +s:find(".*.*.*.*.*.*.*.*.*x") |
| 420 | +``` |
| 421 | + |
| 422 | + |
| 423 | +限制对字符串库访问的一种有趣方法,是使用调用钩子。每次调用某个函数时,我们都会检查该函数是否经过授权。下图 25.6 “使用钩子禁止对未授权函数的调用”,实现了这一想法。 |
| 424 | + |
| 425 | + |
| 426 | +<a name="f-25.6"></a> **使用钩子禁止对未授权函数的调用** |
| 427 | + |
| 428 | +{{#include scripts/reflection/demo_call_hook.lua}} |
| 429 | + |
| 430 | + |
| 431 | +在该代码中,表 `validfunc` 表示程序可以调用函数的集合。其中函数 `hook` 使用了 `debug` 库访问正被调用的函数,然后检查该函数是否在 `validfunc` 集合中。 |
395 | 432 |
|
396 | 433 |
|
0 commit comments