Lua(Codea) 中 table.insert 越界錯誤緣由分析

Lua(Codea) 中 table.insert(touches, touch.id, touch) 越界錯誤緣由分析

背景介紹

Codea 上運行其餘人之前寫的代碼時, 發現某段處理 touch 事件的代碼老是報錯, 開始報浮點數沒有整型的表示, 修改代碼增長類型轉換後, 又報越界錯誤.git

試驗代碼

由於這些程序在以前版本的 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 中報錯, 運行信息以下:ide

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 中的表現ui

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
>

結論

能夠看出:lua

  • table.insert 時空表必須從 1 開始, 後面的索引要跟前一個保持連續.
  • 123 僅僅被當成 my 中哈希表的 key, 而不是數組索引.
  • 計算長度時沒有把以哈希表方式存儲的項目算進去

分析Lua源代碼

開始懷疑多是 touch.id 數字太大, 後來發現改用小數字也不行, 幸虧 lua 提供了源代碼, 用 git grep -n "報錯信息"lua-5.3.2 的源代碼中順利找到對應的函數代碼, 發現確實有一個條件判斷, 查詢結果以下:code

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     else
 56       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);
 ...

--結束

相關文章
相關標籤/搜索