Skip to content

Commit 9950fe6

Browse files
committed
Updated 'src/reflection.md'.
1 parent 2cd65aa commit 9950fe6

File tree

8 files changed

+144
-30
lines changed

8 files changed

+144
-30
lines changed

scripts/demo_call_hook.lua

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
local debug = require "debug"
2+
3+
-- 可执行的最大 “步数”
4+
local steplimit = 1000
5+
6+
local count = 0 -- 步骤计数器
7+
8+
-- 已授权函数的集合
9+
local validfunc = {
10+
[string.upper] = true,
11+
[string.lower] = true,
12+
... -- 其他已授权函数
13+
}
14+
15+
local function hook (event)
16+
if event == "call" then
17+
local info = debug.getinfo(2, "fn")
18+
if not validfunc[info.func] then
19+
error("正调用不良函数:" .. (info.name or "?"))
20+
end
21+
end
22+
23+
count = count + 1
24+
if count > steplimit then
25+
error("脚本使用了过多 CPU")
26+
end
27+
end
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
local debug = require "debug"
2+
3+
-- 可执行的最大 “步数”
4+
local steplimit = 1000
5+
local count = 0 -- 步骤计数器
6+
-- 已授权函数的集合
7+
local validfunc = {
8+
[string.upper] = true,
9+
[string.lower] = true,
10+
-- ... -- 其他已授权函数
11+
}
12+
13+
local function hook (ev)
14+
if ev == "call" then
15+
local info = debug.getinfo(2, "fn")
16+
if not validfunc[info.func] then
17+
error("正调用不良函数:" .. (info.name or "?"))
18+
end
19+
end
20+
21+
count = count + 1
22+
if count > steplimit then
23+
error("脚本使用了过多 CPU")
24+
end
25+
end
26+
27+
-- 加载代码块
28+
local f = assert(loadfile(arg[1], "t", {}))
29+
debug.sethook(hook, "", 100) -- 设置钩子
30+
f() -- 运行代码块
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
local debug = require "debug"
2+
3+
local a = 1
4+
5+
print(debug.getinfo(a).what)
6+
print(debug.getinfo(a).nups)
7+
print(debug.getinfo(a).nparams)
8+
print(debug.getinfo(a).isvararg)
9+
print(debug.getinfo(a).activelines)
10+
print(debug.getinfo(a).linedefined)
11+
print(debug.getinfo(a).lastlinedefined)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
local s = "123456789012345"
2+
for i = 1, 36 do s = s .. s end

scripts/reflection/str_man.lua

Whitespace-only changes.

src/lua_tutorial.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55

66
因为在 [Slurm 集群负载调度器](https://slurm.schedmd.com/) 部署中,会用到用 Lua 实现的 [Lmod](https://github.com/TACC/Lmod) 这个环境模块系统,故有必要了解 Lua 这门编程语言。
77

8+
**相关资源**
9+
10+
- [ZeroBrane Studio](https://studio.zerobrane.com/) 是款轻量级的 Lua 集成开发环境 IDE,支持Lua 5.1、Lua 5.2、Lua 5.3、Lua 5.4、LuaJIT 及其他 Lua 引擎的代码自动补全、语法高亮、实时编码、代码分析与调试等。其代码位于 `/usr/share/zbstudio` 目录下,需要修改该目录权限为 `0755`
11+
12+
- [williamwilling/luagui](https://github.com/williamwilling/luagui) 是个用 Lua 创建图形用户界面的库,主要面向没有经验的程序员。该库的开发目标:易于使用;使用 Lua 的通用约定。(不应该让人感觉像是对 C++ 库的封装)。
13+
14+
815

916
## 前言
1017

src/reflection.md

Lines changed: 67 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,93 +5,111 @@
55

66
反射,是指程序检查、修改其自身执行某些方面的能力。动态语言(如 Lua)自然而然支持多种反射特性:
77

8-
- 环境特性允许了运行时的全局变量检查
9-
- 函数(如 `type` `pairs`)允许了运行时的未知数据结构检查和遍历
10-
- 函数(如 `load` `require`)允许了程序为自身添加代码或更新自己的代码
8+
- 环境特性允许运行时的全局变量检查
9+
- 诸如 `type` `pairs` 等函数,允许运行时的未知数据结构检查和遍历
10+
- 而诸如 `load` `require` 等函数,则允许程序为自身添加代码,或更新自己的代码
1111

1212

13-
然而,仍有许多不足之处:程序无法自省其局部变量,程序无法跟踪其执行情况,函数无法获悉其调用者,等等。调试库,the debug library,填补了这些空白。
13+
然而,仍缺少许多东西:
1414

15+
- 程序无法自省其局部变量;
16+
- 程序无法跟踪其执行情况;
17+
- 函数无法获悉其调用者等等。
1518

16-
调试库包含两类函数:*内省函数,introspective functions**钩子,hooks*。内省函数允许我们检查正在运行程序的多个方面,譬如活动函数堆栈、当前执行行及局部变量的值和名称等。钩子允许我们跟踪程序的执行。
1719

20+
调试库,the `debug` library,填补了这些空白。
1821

19-
尽管名为调试库,但他并未提供一个 Lua 调试器。不过,他提供了编写我们自己调试器所需的,复杂程度各不相同的全部原语。
2022

23+
调试库包含两类函数:
2124

22-
与其他库不同,我们应该谨慎使用调试库,use the debug library with parsimony。首先,调试库的某些功能,并不以性能著称。其次,他打破了该门语言的一些神圣真理,比如我们不能从局部变量的词法范围之外,访问该局部变量这一条。虽然调试库与标准库一样直接可用,但我(作者)更倾向于,在任何用到调试库的代码块中,显式地导入他。
25+
- *内省函数,introspective functions*
26+
-*钩子,hooks*
2327

2428

29+
内省函数允许我们检查正在运行程序的多个方面,譬如活动函数堆栈、当前执行行及局部变量的值和名称等。钩子则允许我们跟踪程序的执行。
30+
31+
32+
尽管名为调试库,但他并未提供给我们一个 Lua 调试器。不过,他提供了编写我们自己调试器所需的、复杂程度各不相同的全部原语。
33+
34+
35+
与其他库不同,我们应谨慎使用调试库,use the debug library with parsimony。首先,调试库的某些功能,并不以性能著称。其次,他打破了该门语言的一些神圣真理,比如我们不能从局部变量的词法范围外,访问该局部变量这一条。虽然调试库与标准库一样直接可用,但我(作者)更倾向于,在任何用到调试库的代码块中,显式地导入他。
36+
2537

2638
## 自省设施
2739

2840
**Introspective Facilities**
2941

3042

31-
调试库中的主要自省函数,是 `getinfo`。其第一个参数可以是某个函数,也可以是某个堆栈层级,a stack level。当我们对函数 `foo` 调用 `debug.getinfo(foo)` 时,他会返回一个包含有关该函数一些数据的表。该表可以包含以下字段
43+
调试库中的主要自省函数为 `getinfo`。其第一个参数可以是某个函数,或某个堆栈层级,a stack level。当我们对某个函数 `foo`调用 `debug.getinfo(foo)` 时,他会返回一个包含有着该函数一些数据的表。该表可能包含以下字段
3244

3345

34-
- `source`:该字段给出函数于何处定义。如果函数是在字符串中定义的(经由一个 `load` 调用),那么 `source` 就是那个字符串。如果函数是在某个文件中定义的,那么 `source` 就是以 `@` 符号为前缀的文件名;
46+
- `source`:该字段给出函数于何处定义。如果该函数定义在某个字符串中(经由一次 `load` 调用),`source` 就是那个字符串。如果函数定义在在某个文件中,`source` 就是以 `@` 符号为前缀的文件名;
3547

36-
- `short_src`:该字段是 `source` 简短版本(最多 60 个字符)。这对于错误信息非常有用
48+
- `short_src`:该字段是 `source` 简短版本(最多 60 个字符)。这对于错误消息非常有用
3749

38-
- `linedefined`该字段给出了定义函数的源代码第一行编号
50+
- `linedefined`该字段给出来源中定义该函数处的第一行编号
3951

40-
- `lastlinedefined`该字段给出了定义函数的源代码最后一行编号
52+
- `lastlinedefined`该字段给出来源中定义该函数处的最后一行编号
4153

42-
- `what`该字段给出了此函数是什么。如果 `foo` 是个常规 Lua 函数,则选项为 `Lua`;如果是个 C 函数,则选项为 `C`如果是 Lua 代码块的主要部分,则选项为 `main`
54+
- `what`该字段给出了该函数是什么。如果 `foo` 是个常规 Lua 函数,则选项为 `Lua`;如果是个 C 函数,则选项为 `C`如果该函数是某个 Lua 代码块的主要部分,则为 `main`
4355

44-
- `name`该字段给到函数的合理名称,例如存储该函数的全局变量名称
56+
- `name`该字段给出一个该函数的合理名称,比如存储该函数的全局变量名字
4557

46-
- `namewhat`:该字段给出前一字段的含义。该字段可以是 `global``local``method``field``''`(空字符串)。空字符串表示 Lua 没有找到函数名称
58+
- `namewhat`:该字段给出前一字段的含义。该字段可以是 `global``local``method``field``''`(空字符串)。空字符串表示 Lua 没有找到该函数的名字
4759

4860
- `nups`:这是该函数的上值个数,the number of upvalues;
4961

5062
- `nparams`:这是该函数的参数个数;
5163

52-
- `isvararg`这表明函数是否为可变参数,whether the function is variadic(一个布尔值)
64+
- `isvararg`这表示该函数是否为可变参数(一个布尔值),whether the function is variadic;
5365

54-
- `activelines`该字段是个表示函数活动行集合的表。所谓 *活动行,active line*,是指有代码的行,而不是空行或仅包含注释的行。(该信息的一个典型用途,是设置断点。大多数调试器不允许我们在活动行之外设置断点,因为这样的断点是无法到达的。)
66+
- `activelines`该字段是个表示函数活动行的集合的表。所谓 *活动行,active line*,是指有代码的行,而不是空行或仅包含注释的行。(此信息的一个典型用途是设置断点。大多数调试器都不允许我们在活动行之外设置断点,因为这样的断点是无法到达的。)
5567

5668
- `func`:该字段为函数本身。
5769

5870

5971
`foo` 是个 C 语言函数时,Lua 就没有太多关于他的数据。对于此类函数,只有 `what``name``namewhat``nups``func` 字段是有意义的。
6072

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`。(在这种情况下,该函数的真正调用者已不在堆栈上了。)
6277

63-
`name` 字段很棘手。请记住,由于函数在 Lua 中属于头等值,functions are first-class values in Lua,因此函数可能没有名字,也可能有多个名字。Lua 尝试通过查看调用函数的代码,了解某个函数是如何被调用的,来找到该函数的名字。这种方法只有在我们调用带有数字的 `getinfo` ,即在我们请求有关某个特定调用的信息时,才会起作用。
78+
`name` 字段很棘手。请记住,由于函数是 Lua 中的头等值,functions are first-class values in Lua,因此函数可能没有名字,也可能有多个名字。Lua 通过查看调用函数代码,了解某个函数是如何被调用的,尝试找到该函数的名字。这种方式只有在我们以某个数字调用 `getinfo` ,即在我们请求有关某个特定调用的信息时,才会起作用。
6479

65-
函数 `getinfo` 并不高效。Lua 以不影响程序执行的形式,保存调试信息;高效检索是次要目标。为获得更好性能`getinfo` 有个可选的第二参数,用于选择要获取的信息。这样,该函数就不会浪费时间,收集用户不需要的数据。该参数的格式是个字符串,每个字母代表一组字段,如下表所示。
80+
函数 `getinfo` 并不高效。Lua 以不影响程序执行的形式,保存调试信息;高效检索是次要目标。为达到更好性能`getinfo` 有个用于选取要获得信息的可选的第二个参数。这样,该函数就不会浪费时间收集用户不需要的数据。该参数的格式是个字符串,其中每个字母会选取一组字段,如下表所示。
6681

6782
| 选项 | 意义 |
6883
| :-: | :- |
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` |
7590

7691

77-
以下函数通过打印活动堆栈的原始回溯,说明了 `debug.getinfo` 用法:
92+
下面这个函数通过打印出活动堆栈的原始回溯,说明了 `debug.getinfo` 用法:
7893

7994

8095
```lua
8196
function traceback ()
8297
for level = 1, math.huge do
8398
local info = debug.getinfo(level, "Sl")
99+
84100
if not info then break end
101+
85102
if info.what == "C" then -- 是个 C 函数?
86103
print(string.format("%d\tC 函数", level))
87104
else -- 是个 Lua 函数
88105
print(string.format("%d\t[%s]:%d", level, info.short_src, info.currentline))
89106
end
107+
90108
end
91109
end
92110
```
93111

94-
要改进这个函数并不难,只要包含更多 `getinfo` 中的数据即可。实际上,调试库就提供了这样一个改进版本,即函数 `traceback`。与我们的版本不同,`debug.traceback` 并不打印结果而是返回一个包含回溯信息的字符串(可能很长):
112+
要改进这个函数并不难,只要包含更多 `getinfo` 中的数据即可。实际上,调试库就提供了这样一个改进版本,即函数 `traceback`。与我们的版本不同,`debug.traceback` 并不打印结果而是返回一个包含回溯信息的字符串(可能很长):
95113

96114

97115
```console
@@ -385,12 +403,31 @@ for i = 1, 36 do s = s .. s end
385403

386404
<a name="f-25.5"></a> **控制内存使用**
387405
```lua
388-
{{#include ../scripts/improved_sandbox.lua:3:22}}
406+
{{#include ../scripts/reflection/improved_sandbox.lua:3:22}}
389407

390408
-- 如前
391409
```
392410

393411

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` 集合中。
395432

396433

0 commit comments

Comments
 (0)