前段时间,在给我们游戏服务器写 lua 的脚本的时候,发现了一个奇怪的现象,一段 lua 代码只要一执行就把服务器给搞挂了,仔细分析了一下,发现这段 lua 代码并没有执行什么特别的操作,甚至都没有跟我们服务器的 C++ 层交互,仅仅只是使用 lua 自身的一些库函数,而且只对 windows 平台下的服务端会产生这个崩溃。初步认为是 windows 平台的原因。于是我在 windows 平台下编译了 lua 的源码,跟进去后发现原来是宕在了 windows 的 CRT 函数里,解释一下 CRT 是 windows 的 C 运行时库,如果有朋友不清楚,可以看看 《程序员自我修养》 这本书中关于运行时库的章节。
接着说我的问题,宕在了一次调用 lua 内置的 os 库的 date() 函数,调用如下:
os.date("%C")
跟进lua源码后发现,源码里最后调用了 C 库的 strftime 函数:
static int os_date (lua_State *L) {
const char *s = luaL_optstring(L, 1, "%c");
time_t t = luaL_opt(L, (time_t)luaL_checknumber, 2, time(NULL));
struct tm *stm;
if (*s == '!') { /* UTC? */
stm = gmtime(&t);
s++; /* skip `!' */
}
else
stm = localtime(&t);
if (stm == NULL) /* invalid date? */
lua_pushnil(L);
else if (strcmp(s, "*t") == 0) {
lua_createtable(L, 0, 9); /* 9 = number of fields */
setfield(L, "sec", stm->tm_sec);
setfield(L, "min", stm->tm_min);
setfield(L, "hour", stm->tm_hour);
setfield(L, "day", stm->tm_mday);
setfield(L, "month", stm->tm_mon+1);
setfield(L, "year", stm->tm_year+1900);
setfield(L, "wday", stm->tm_wday+1);
setfield(L, "yday", stm->tm_yday+1);
setboolfield(L, "isdst", stm->tm_isdst);
}
else {
char cc[3];
luaL_Buffer b;
cc[0] = '%'; cc[2] = '\0';
luaL_buffinit(L, &b);
for (; *s; s++) {
if (*s != '%' || *(s + 1) == '\0') /* no conversion specifier? */
luaL_addchar(&b, *s);
else {
size_t reslen;
char buff[200]; /* should be big enough for any conversion result */
cc[1] = *(++s);
reslen = strftime(buff, sizeof(buff), cc, stm);
luaL_addlstring(&b, buff, reslen);
}
}
luaL_pushresult(&b);
}
return 1;
}
因为我们的服务端编的都是 debug 版本,vs 编 debug 版默认使用的多线程的那个 debug 版本的库:
打开源码找到对应的 strftime 函数,然后一路跟进去,最后终于找到了罪魁祸首!尼玛居然有一行 ASSERT!
最后就因为这个 ASSERT,我们的服务端光荣的挂了,而且我们外网的服务端就用的是 debug 版本,不过幸好 glibc 的库下不存在这个问题。
既然都看到这里了,我就顺带对比了一下,vs 这个版本的 CRT 库里 strftime 能够支持的字符和标准有什么区别:
CRT 能够支持的字符有:aAbBcdHIjmMpSUwWxXyYZz%’\004’‘\015’
而最新的 c 和 c++ 可以支持的果然更多,可以在 cplusplus.com 对比一下。