2019独角兽企业重金招聘Python工程师标准>>>
Lua(Codea) 中 table.insert(touches, touch.id, touch) 越界错误原因分析
背景介绍
在 Codea
上运行其他人以前写的代码时, 发现某段处理 touch
事件的代码总是报错, 开始报浮点数没有整型的表示, 修改代码增加类型转换后, 又报越界错误.
试验代码
因为这些程序在之前版本的 Codea
可以正常运行(使用 lua-5.1), 所以我推测这个错误可能是 lua
版本差异引发的. 为方便定位问题, 从 iPad
转到 树莓派
的 lua-5.3.2
环境进行试验(因为目前最新版本的 Codea 对应的 Lua 版本是 5.3), Codea
中的试验代码如下:
代码1
touches = {}
touch={id=100}
table.insert(touches, math.floor(touch.id), touch)
在 Lua-5.3.2
中报错, 运行信息如下:
pi@rpi /opt/software/lua-5.3.2 $ lua
Lua 5.3.2 Copyright (C) 1994-2015 Lua.org, PUC-Rio
>
> touches = {}
> touch={id=100}
> table.insert(touches, math.floor(touch.id), touch)
stdin:1: bad argument #2 to 'insert' (position out of bounds)
stack traceback:[C]: in function 'table.insert'stdin:1: in main chunk[C]: in ?
>
在 Lua-5.1.5
中正常运行, 运行信息如下:
pi@rpi /opt/software $ lua5.1
Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio
>
> touches = {}
> touch={id=100}
> table.insert(touches, math.floor(touch.id), touch)
>
代码2
在 Lua-5.3.2
中报错, 运行信息如下:
kano@kano ~ $ lua
Lua 5.3.2 Copyright (C) 1994-2015 Lua.org, PUC-Rio
> my={}
> table.insert(my,123,12)
stdin:1: bad argument #2 to 'insert' (position out of bounds)
stack traceback:[C]: in function 'table.insert'stdin:1: in main chunk[C]: in ?
> table.insert(my,1,12)
> table.insert(my,2,12)
> table.insert(my,4,12)
stdin:1: bad argument #2 to 'insert' (position out of bounds)
stack traceback:[C]: in function 'table.insert'stdin:1: in main chunk[C]: in ?
> my[123]=123
> #my
2
> unpack(my)
stdin:1: attempt to call a nil value (global 'unpack')
stack traceback:stdin:1: in main chunk[C]: in ?
> table.unpack(my)
12 12
> for k,v in pairs(my) do print(k,v) end
1 12
2 12
123 123
>
再看看 5.1
中的表现
pi@rpi /opt/software/lua-5.3.2/src $ lua
Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio
> my={}
> table.insert(my,123,12)
> #my
stdin:1: unexpected symbol near '#'
> table.length(my)
stdin:1: attempt to call field 'length' (a nil value)
stack traceback:stdin:1: in main chunk[C]: ?
> table.len(my)
stdin:1: attempt to call field 'len' (a nil value)
stack traceback:stdin:1: in main chunk[C]: ?
> table.insert(my,1,12)
> table.insert(my,2,12)
> table.insert(my,4,12)
> my[123]=123
> print(#my)
4
> table.unpack(my)
stdin:1: attempt to call field 'unpack' (a nil value)
stack traceback:stdin:1: in main chunk[C]: ?
> for k,v in pairs(my) do print(k,v) end
1 12
2 12
4 12
123 123
>
结论
可以看出:
- 用
table.insert
时空表必须从1
开始, 后面的索引要跟前一个保持连续. 123
仅仅被当成my
中哈希表的key
, 而不是数组索引.- 计算长度时没有把以哈希表方式存储的项目算进去
分析Lua源代码
开始怀疑可能是 touch.id
数字太大, 后来发现改用小数字也不行, 幸好 lua
提供了源代码, 用 git grep -n "报错信息"
在 lua-5.3.2
的源代码中顺利找到对应的函数代码, 发现确实有一个条件判断, 查询结果如下:
pi@rpi /opt/software/lua-5.3.2 $ git grep -n "position out of bounds"
src/ltablib.c:90: luaL_argcheck(L, 1 <= pos && pos <= e, 2, "position out of bounds");
src/ltablib.c:110: luaL_argcheck(L, 1 <= pos && pos <= size + 1, 1, "position out of bounds");
pi@rpi /opt/software/lua-5.3.2 $
查询结果很明确, 该错误信息可在源文件 src/ltablib.c
的第 90
行和第 110
行找到, 用 vi
打开该文件, 在 vi
命令模式下输入 :90
, 即可跳转到第 90
行, 发现是一个 table.insert
函数, 第 110
行是一个 table.remove
函数, 代码如下:
79 static int tinsert (lua_State *L) {80 lua_Integer e = aux_getn(L, 1, TAB_RW) + 1; /* first empty element */81 lua_Integer pos; /* where to insert new element */82 switch (lua_gettop(L)) {83 case 2: { /* called with only 2 arguments */84 pos = e; /* insert new element at the end */85 break;86 }87 case 3: {88 lua_Integer i;89 pos = luaL_checkinteger(L, 2); /* 2nd argument is the position */90 luaL_argcheck(L, 1 <= pos && pos <= e, 2, "position out of bounds");91 for (i = e; i > pos; i--) { /* move up elements */92 lua_geti(L, 1, i - 1);93 lua_seti(L, 1, i); /* t[i] = t[i - 1] */94 }95 break;96 }97 default: {98 return luaL_error(L, "wrong number of arguments to 'insert'");99 }
100 }
101 lua_seti(L, 1, pos); /* t[pos] = v */
102 return 0;
103 }
104
105
106 static int tremove (lua_State *L) {
107 lua_Integer size = aux_getn(L, 1, TAB_RW);
108 lua_Integer pos = luaL_optinteger(L, 2, size);
109 if (pos != size) /* validate 'pos' if given */
110 luaL_argcheck(L, 1 <= pos && pos <= size + 1, 1, "position out of bounds");
111 lua_geti(L, 1, pos); /* result = t[pos] */
112 for ( ; pos < size; pos++) {
113 lua_geti(L, 1, pos + 1);
114 lua_seti(L, 1, pos); /* t[pos] = t[pos + 1] */
115 }
116 lua_pushnil(L);
117 lua_seti(L, 1, pos); /* t[pos] = nil */
118 return 1;
119 }
读读代码, 发现这里的两个函数都用 luaL_argcheck
对参数做了检查, 如果合法则通过, 如果不合法则返回错误信息.
在函数 tinsert
中的合法条件是 1 <= pos && pos <= e
, 那么 e
是多少呢? 继续看代码, 在函数最开始有定义, 还有注释:
lua_Integer e = aux_getn(L, 1, TAB_RW) + 1; /* first empty element */
表中的第一个空元素的位置索引(也就是最后一个位置+1).
接着看一下在函数 tremove
中的判断条件: 1 <= pos && pos <= size + 1
, 其中的 size
也在函数最开始有定义, 跟函数 tinsert
中的 e
完全一样:
lua_Integer size = aux_getn(L, 1, TAB_RW);
相关的几个定义:
27 #define TAB_R 1 /* read */28 #define TAB_W 2 /* write */29 #define TAB_L 4 /* length */30 #define TAB_RW (TAB_R | TAB_W) /* read/write */31 32 33 #define aux_getn(L,n,w) (checktab(L, n, (w) | TAB_L), luaL_len(L, n))34 35 36 static int checkfield (lua_State *L, const char *key, int n) {37 lua_pushstring(L, key);38 return (lua_rawget(L, -n) != LUA_TNIL);39 }40 41 42 /*43 ** Check that 'arg' either is a table or can behave like one (that is,44 ** has a metatable with the required metamethods)45 */46 static void checktab (lua_State *L, int arg, int what) {47 if (lua_type(L, arg) != LUA_TTABLE) { /* is it not a table? */48 int n = 1; /* number of elements to pop */49 if (lua_getmetatable(L, arg) && /* must have metatable */50 (!(what & TAB_R) || checkfield(L, "__index", ++n)) &&51 (!(what & TAB_W) || checkfield(L, "__newindex", ++n)) &&52 (!(what & TAB_L) || checkfield(L, "__len", ++n))) {53 lua_pop(L, n); /* pop metatable and tested metamethods */54 }55 else56 luaL_argerror(L, arg, "table expected"); /* force an error */57 }58 }
现在我们明白这个判断条件的意思了, 就是对第二个参数(插入位置索引/删除位置索引)进行判断, 如果它超出当前表的大小, 那么就返回错误.
这种表现明显跟我们以前版本的 lua
不一样, 以前(5.1)可以任意取一个位置索引进行插入, 比如这样:
pi@rpi /opt/software $ lua5.1
Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio
> touches = {}
> touch={id=100}
> table.insert(touches, 1000000, touch)
>
那么我们看看 5.1
中这两个函数(tinsert/tremove)的源代码:
90 static int tinsert (lua_State *L) {91 int e = aux_getn(L, 1) + 1; /* first empty element */92 int pos; /* where to insert new element */93 switch (lua_gettop(L)) {94 case 2: { /* called with only 2 arguments */95 pos = e; /* insert new element at the end */96 break;97 }98 case 3: {99 int i;
100 pos = luaL_checkint(L, 2); /* 2nd argument is the position */
101 if (pos > e) e = pos; /* `grow' array if necessary */
102 for (i = e; i > pos; i--) { /* move up elements */
103 lua_rawgeti(L, 1, i-1);
104 lua_rawseti(L, 1, i); /* t[i] = t[i-1] */
105 }
106 break;
107 }
108 default: {
109 return luaL_error(L, "wrong number of arguments to " LUA_QL("insert"));
110 }
111 }
112 luaL_setn(L, 1, e); /* new size */
113 lua_rawseti(L, 1, pos); /* t[pos] = v */
114 return 0;
115 }
116
117
118 static int tremove (lua_State *L) {
119 int e = aux_getn(L, 1);
120 int pos = luaL_optint(L, 2, e);
121 if (!(1 <= pos && pos <= e)) /* position is outside bounds? */
122 return 0; /* nothing to remove */
123 luaL_setn(L, 1, e - 1); /* t.n = n-1 */
124 lua_rawgeti(L, 1, pos); /* result = t[pos] */
125 for ( ;pos<e; pos++) {
126 lua_rawgeti(L, 1, pos+1);
127 lua_rawseti(L, 1, pos); /* t[pos] = t[pos+1] */
128 }
129 lua_pushnil(L);
130 lua_rawseti(L, 1, e); /* t[e] = nil */
131 return 1;
132 }
很显然, 在 5.1
中对位置索引的判断处理不太一样:
if (pos > e) e = pos; /* `grow' array if necessary */
如果索引位置大于当前最大位置, 则把索引位置赋给当前最大位置, 相当于扩大了表, 这是一个可以动态"生长"的数组, 这样的话可能需要分配更多的无用空间. 也许出于优化考虑, 在 5.3
中不允许这么做了. 所以就让我们以前正常的代码出错了.
更多代码细节
如果想了解更清楚, 可以在源代码里搜索一下函数(或者宏) luaL_argcheck
:
pi@rpi /opt/software/lua-5.3.2 $ git grep -n "luaL_argcheck"
...
src/lauxlib.h:114:#define luaL_argcheck(L, cond,arg,extramsg) \
...
看样子是个宏, 打开 src/lauxlib.h
, 查到如下宏定义:
114 #define luaL_argcheck(L, cond,arg,extramsg) \
115 ((void)((cond) || luaL_argerror(L, (arg), (extramsg))))
发现又调用了一个 luaL_argerror
, 先在本文件里查一下, 发现有函数声明:
38 LUALIB_API int (luaL_argerror) (lua_State *L, int arg, const char *extramsg);
那么函数定义应该在 src/lauxlib.c
中, 再用 git grep -n
搜一把, 如下:
pi@rpi /opt/software/lua-5.3.2 $ git grep -n "luaL_argerror"
...
src/lauxlib.c:164:LUALIB_API int luaL_argerror (lua_State *L, int arg, const char *extramsg) {
...
很好, 打开看看具体代码:
164 LUALIB_API int luaL_argerror (lua_State *L, int arg, const char *extramsg) {165 lua_Debug ar;166 if (!lua_getstack(L, 0, &ar)) /* no stack frame? */167 return luaL_error(L, "bad argument #%d (%s)", arg, extramsg);168 lua_getinfo(L, "n", &ar);169 if (strcmp(ar.namewhat, "method") == 0) {170 arg--; /* do not count 'self' */171 if (arg == 0) /* error is in the self argument itself? */172 return luaL_error(L, "calling '%s' on bad self (%s)",173 ar.name, extramsg);174 }175 if (ar.name == NULL)176 ar.name = (pushglobalfuncname(L, &ar)) ? lua_tostring(L, -1) : "?";177 return luaL_error(L, "bad argument #%d to '%s' (%s)",178 arg, ar.name, extramsg);179 }
看得出来, 我们的试验代码触发了最后一条判断语句:
...175 if (ar.name == NULL)176 ar.name = (pushglobalfuncname(L, &ar)) ? lua_tostring(L, -1) : "?";177 return luaL_error(L, "bad argument #%d to '%s' (%s)",178 arg, ar.name, extramsg);...
--结束