很抱歉,这周实在是太忙了。。。不废话,直接进入主题。
所谓的分析思路,无非就是找到程序的入口点,按执行流程一步一步地阅读分析,或者从自己感兴趣的部分入手。这个系列暂时还不是对Lua的全面剖析,只是选取其中一部分来分析。因此我们要找到自己感兴趣的部分,有针对性地进行分析。
由于Lua的官方文档比较齐全,浏览一下官方提供的资料集会比较便于我们着手分析。
所谓的源码分析,就是从源代码构建可执行程序,调试调试,看看执行流程,看看主要的数据结构和算法、程序的运行状态之类的,最后再品味一下设计(问题背景、原因、优缺点),仅此而已。
那我们先来看看源代码的布局。
可以从Makefile文件入手,不过这里用的是Windows + Visual Studio,可以省去这个步骤。看看从官方下载的源码包,提供了以下文件:
./lua-5.3.0/ Makefile README /doc/ contents.html logo.gif lua.1 lua.css luac.1 manual.css manual.html osi-certified-72x60.png readme.html /src/ lapi.c lapi.h lauxlib.c lauxlib.h lbaselib.c lbitlib.c lcode.c lcode.h lcorolib.c lctype.c lctype.h ldblib.c ldebug.c ldebug.h ldo.c ldo.h ldump.c lfunc.c lfunc.h lgc.c lgc.h linit.c liolib.c llex.c llex.h llimits.h lmathlib.c lmem.c lmem.h loadlib.c lobject.c lobject.h lopcodes.c lopcodes.h loslib.c lparser.c lparser.h lprefix.h lstate.c lstate.h lstring.c lstring.h lstrlib.c ltable.c ltable.h ltablib.c ltm.c ltm.h lua.c lua.h lua.hpp luac.c luaconf.h lualib.h lundump.c lundump.h lutf8lib.c lvm.c lvm.h lzio.c lzio.h Makefile
第一件事当然是看README啦还用说,当然专业的做法可以看Makefile,这里为了避免引入其他无关知识,还是选用简单的方法。
按照说明查看doc/readme.html文件其中的【Building Lua on other systems】节,我们发现它介绍了lua可执行程序大致的组成和依赖关系如下:
library: lapi.c lcode.c lctype.c ldebug.c ldo.c ldump.c lfunc.c lgc.c llex.c lmem.c lobject.c lopcodes.c lparser.c lstate.c lstring.c ltable.c ltm.c lundump.c lvm.c lzio.c lauxlib.c lbaselib.c lbitlib.c lcorolib.c ldblib.c liolib.c lmathlib.c loslib.c lstrlib.c ltablib.c lutf8lib.c loadlib.c linit.c interpreter: library, lua.c compiler: library, luac.c
根据这个简单的依赖关系,我用VS2015建立了相应的工程,便于调试。其实官方资料集里也提供了现成的VS工程的下载链接:
TODO: 此处应有下载链接^_^
因为我们分析的重点是 编译原理和虚拟机的部分 ,而不是相关的库的实现部分。因此应该从 lua.c 或 luac.c 开始入手,其实我们从文件列表中也可以看出,里面有几个比较重要的文件:
llex.c lopcodes.c lparser.c lvm.c
到底从哪里入手比较好,就见仁见智了。这里我还是采用了官方提供的资料来帮助选择。
官方的资料集的wiki中提供了一个 页面 。该页面介绍了这些文件的用途、编程约定、模块结构等等。
为了避免这个页面失效,下面还是可耻地复制粘贴略带翻译地提供给大家,网上也有一些翻译,只是他们翻译时省略掉了一些我觉得有用的信息。个别简单的,我就不翻译了。
ldebug.c - 调试接口。包括以下功能:
访问调试钩子(lua_sethook, lua_gethook, lua_gethookcount),
访问运行时栈信息 (lua_getstack / lua_getlocal / lua_setlocal),
检查字节码(luaG_checkopenop / luaG_checkcode),
引发错误(luaG_typeerror / luaG_concaterror / luaG_aritherror /luaG_ordererror / luaG_errormsg / luaG_runerror)
lzio.c - 一种缓冲输入流接口。
lmem.c - 内存管理接口。它实现了这些内存分配函数:luaM_realloc / luaM_growaux_
lgc.c - 增量式GC (内存管理)
lstate.c - 全局状态。包括:
用于打开和关闭Lua states的函数(lua_newstate/lua_close)
线程相关函数 (luaE_newthread / luaE_freethread)
lobject.c - 一些针对Lua对象的通用函数。包括:
数据类型与其字符串形式的互相转换
原始数据相等性测试(luaO_rawequalObj)
日志基础设施2(luaO_log2)
lstring.c - string table (持有由Lua处理的所有字符串)
lfunc.c - 用来操纵原型和闭包的辅助函数。
ltable.c - Lua tables (hash)
lcode.c - Lua的代码生成器. 由 lparser.c 来使用
llex.c - 词法分析器. 由 lparser.c 来使用
lparser.c - Lua 解析器.
lundump.c - 加载经过预编译的Lua代码块:
实现了用于加载预编译后的代码块的luaU_undump 函数。
还提供了用来解析函数头的 luaU_header 函数(在luaU_undump()内部被调用)。
ldump.c - 用于保存经过预编译的Lua代码块:
实现了用于转储函数对象的luaU_dump()函数。这种函数对象是从文件或字符串预编译而来的Lua代码块。
lopcodes.c - 由Lua虚拟机使用的操作码。
通过 luaP_opnames 和 luaP_opmodes这两个映射表,定义了所有操作码的名称和相关信息。
lvm.c - Lua虚拟机:
用于执行字节码 (luaV_execute).
还暴露了少量函数供 lapi.c 使用(如:luaV_concat).
ldo.c - Lua函数调用和栈管理。处理函数调用 (luaD_call / luaD_pcall), 栈生长, 协程处理等ltm.c - 标签方法(tag methods)。 实现了查询对象中的元方法的功能。
lbaselib.c - (base functions)
lstrlib.c - string
ltablib.c - table
lmathlib.c - math
loslib.c - os
liolib.c - io
loadlib.c - package
ldblib.c - debug
lapi.c - Lua API. Lua C API的主要实现部分(lua_* functions).
lauxlib.c - 定义了 luaL_* 函数
linit.c - 实现了 luaL_openlibs 用于从C语言环境中加载上述模块。
lua.c - 独立的Lua 解析器
print.c - 定义了 "PrintFunction?" 函数,这些函数用于打印字节码 (通过luac.c "-l" 选项来使用)
luac.c - Lua 编译器 (保存字节码到文件/列出字节码)
搞清楚Lua的模块结构后,我们需要着重分析的模块就出来了:
lua 和 luac 程序的实现模块
处理执行Lua字节码的模块
语法分析与代码生成相关模块
基本数据类型的实现模块
当然,到了后期,如果有时间,我还会再分析一下Lua的垃圾收集器,但最近实在是太忙太忙了。
以下是对 lua-5.3.0/src/Makefile 文件重要部分的节选
LUA_A= liblua.a CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o / lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o / ltm.o lundump.o lvm.o lzio.o LIB_O= lauxlib.o lbaselib.o lbitlib.o lcorolib.o ldblib.o liolib.o / lmathlib.o loslib.o lstrlib.o ltablib.o lutf8lib.o loadlib.o linit.o BASE_O= $(CORE_O) $(LIB_O) $(MYOBJS) LUA_T= lua LUA_O= lua.o LUAC_T= luac LUAC_O= luac.o
根据我们感兴趣的模块,大概是以下目标文件:
ldump.o
lundump.o
ltm.o
lobject.o
lstring.o
ltable.o
lfunc.o
其实还是挺多的,不过比较重要的都用粗体标出来了。大致就是按这个顺序去分析。这些其实也可以直接看源文件中引用的头文件,但那样太麻烦了,并且,Lua源码有时候并不是在文件的顶部写include指令,而是在中间的某个地方,比较蛋疼。看Makefile是比较方便而且专业的做法。我们继续看一下Makefile 文件中上述目标文件依赖关系:
lua.o: lua.c lprefix.h lua.h luaconf.h lauxlib.h lualib.h luac.o: luac.c lprefix.h lua.h luaconf.h lauxlib.h lobject.h llimits.h / lstate.h ltm.h lzio.h lmem.h lundump.h ldebug.h lopcodes.h llex.o: llex.c lprefix.h lua.h luaconf.h lctype.h llimits.h ldo.h / lobject.h lstate.h ltm.h lzio.h lmem.h lgc.h llex.h lparser.h lstring.h / ltable.h ldo.o: ldo.c lprefix.h lua.h luaconf.h lapi.h llimits.h lstate.h / lobject.h ltm.h lzio.h lmem.h ldebug.h ldo.h lfunc.h lgc.h lopcodes.h / lparser.h lstring.h ltable.h lundump.h lvm.h lstate.o: lstate.c lprefix.h lua.h luaconf.h lapi.h llimits.h lstate.h / lobject.h ltm.h lzio.h lmem.h ldebug.h ldo.h lfunc.h lgc.h llex.h / lstring.h ltable.h lparser.o: lparser.c lprefix.h lua.h luaconf.h lcode.h llex.h lobject.h / llimits.h lzio.h lmem.h lopcodes.h lparser.h ldebug.h lstate.h ltm.h / ldo.h lfunc.h lstring.h lgc.h ltable.h lopcodes.o: lopcodes.c lprefix.h lopcodes.h llimits.h lua.h luaconf.h lvm.o: lvm.c lprefix.h lua.h luaconf.h ldebug.h lstate.h lobject.h / llimits.h ltm.h lzio.h lmem.h ldo.h lfunc.h lgc.h lopcodes.h lstring.h / ltable.h lvm.h lcode.o: lcode.c lprefix.h lua.h luaconf.h lcode.h llex.h lobject.h / llimits.h lzio.h lmem.h lopcodes.h lparser.h ldebug.h lstate.h ltm.h / ldo.h lgc.h lstring.h ltable.h lvm.h
冒号左边的.o是编译时生成的目标文件,冒号右边是生成这个目标文件需要的源文件, 表示还没写完,下一行继续写。上一段Makefile代码中,等号左边是一个变量,等号右边是这个变量的值。
Makefile是GNU/Linux系统中用于自动化构建的DSL,供gnu make使用,和Android开发中的.gradle文件作用类似。不过VS和Qt也都使用了各自的Makefile格式和工具。VS有nmake,Qt有qmake,跨平台的有cmake,这里就不展开说了,自己去看文档吧。
现在,我们可以比较有针对性地去看源文件了。
下一期将介绍一下Lua源码中的编程约定(其实还是做个搬运工+翻译工,这里的翻译就凑合着看吧,翻译质量应该没有太大问题)。前面这几期都是比较无聊又不可或缺的,只能忍忍啦。预报一下,后天将更新第三弹。