魔兽地图编辑器吧 关注:65,709贴子:3,846,149
  • 12回复贴,共1

记一次追踪lua调用cj函数

只看楼主收藏回复

Ydwe老早就支持用lua作图了,但是一直没去研究他是怎么导出cj等接口给jass调用的。因为之前参与的项目都是c++底层然后用tolua导出接口给lua调用,心里于是有了个疑问,ydwe是怎么做到把jass接口导出的?莫非暴雪跟acb有不可靠人的秘密?最近比较空虚寂寞冷,昨晚刚好拜读了acb大佬“jass字节码”一文,顿时有了很大热情。刚好ydwe又是开源的(业界良心),带着学习目的下载源码看看ydwe是怎么做到导出接口的。
源码下载好后发现需要vs2019,苦逼的我还在坚守2013,没关系,那就不编译了,先看看代码。一番研究后发现导出jass的入口在open_lua_engine.cpp的open_lua_engine方法,直接看这个方法,其中的register_preload_lib(L, "jass.common", common::open)一看就是他了, register_preload_lib其实是把commom:open这个入口放到lua的加载器列表中,于是在lua调用require “jass.common”的时候就会触发到。很自然就会想到,导出接口的函数应该在commom:open里面。于是进入open方法看,这个函数是这样的

卧槽,好像跟想象的有点不一样,怎么没看见各种各样胶水接口的函数啊,就这么短短几句情何以堪。其实也不难理解,这里就是定义当调用cj的时候会触发的元方法,我们在lua中一般是调用cj,所以看__index触发的方法就好了,没错就是这个jass_get

jass_get里面可以看到,他拿到我们调用的cj函数名字去获取一个nf,进去看看jass_func

注意那个宏那里,里面有个jass_function = initialize_mapping("Deg2Rad");只会初始化一次生成jass_function。这个函数很明显是根据给出的cj函数名去索引对应的方法。关键地方就在如何初始化这个jass_function表,看到他传入的参数是”Deg2Rad”,一个cj函数很熟悉有木有。继续看看这个函数做了啥

这里粗略看是根据Deg2Rad去找到一个地址,然后根据这个地址去遍历,把找到的函数挨个记录在m这个表然后作为返回值(就是jass_function)。
那这到底是怎么个查找法?先来看看start是怎么找出来的。直接看get_war3_searcherd.search_string这个方法,其中get_war3_searcher是获取一个单例对象war3_searcher,值得注意的是这个单例对象是用game.dll初始化的,也就是说,后面的查找都是针对game.dll的查找。search_string中间还封装了一层不截图了比较简单,直接看这个

看到了是先search_string_ptr

用“Deg2Rad”在data段进行查找,找到函数名对应字符串地址:

也就是说现在” Deg2Rad “在game.rdata段的地址是6EF87128, 即ptr=6EF87128,接下来是用ptr在text段进行查找,看看哪里用了这个地址,如果用了那就说明你一定跟”Deg2Rad”有关系(注意PE文件博大精深,本人了解有限,如有错欢迎指正)

看到这个6EF87128就是上面第一步搜索到的

记录下这一行汇编代码对应的地址:6E809A5A,也就是整个search_string的返回值,然后继续看c++代码

这个start=6E809A5B(为啥是5B?,注意5A是整条指令的地址,而找到的Deg2Rad是5B开始,5A是操作码,占用了一个字节),循环里面用start-6=6E809A55地址认为是一个detail::asm_register_native_function对象,看看这个detail::asm_register_native_function是啥:

这个结构体内有4个asm_opcode_5,一个asm_opcode_5占用5个字节,而一个asm_opcode_5就是当前一行汇编语句的长度,其中第一个字节是操作码,剩下的4个字节才是我们要的关键信息(上图绿色字部分是ydwe代码里面的注释,暖心的acb)

意思就是,从6E809A55开始,每4行汇编代码就是一个asm_register_native_function对象,每行汇编代码5个字节。等这个循环结束,就把所有cj函数的关键信息都提取出来了,然后用这些信息又初始化了一个对象func_value放在上面说的m局部变量(就是jass_function)。
func_value结构:

干了这么多活,总算是可以通过cj函数名去拿到func_value对象,继续看这个函数

有了nf后把nf作为upvalue传入,生成闭包,注意闭包中会调用jass_call_closure
看看这个函数

闭包函数主要是红箭头这个函数,看名字就是用刚才传入进去的nf进行一系列的调用。
继续跟进去


这个函数是这样的:先从nf里面获取到这个cj函数到底有几个参数要传入,然后遍历这几个参数把lua堆栈中我们传入的参数转换为魔兽识别的格式,并在param的vertor存起来,完成这些步骤后就来到了:jass::call这个函数

这个函数就是cj函数的调用,大概意思是:
先把esp指针给减了,然后把参数都放进去(memcpy),调用call后,还原原来的esp指针,然后把eax的值放入到retval中(为啥是eax的值?在vc中call的返回值就是放在eax中的)
这步完成后最终来到了
return jass_push(L, nf->get_return(),retval) ? 1 : 0;
这一步是根据cj函数的返回值类型在转换为lua识别的格式,cj函数只有一个返回值,所以这里如果jass_push成功的话则返回1,表示需要返回一个值给lua
整个导出cj接口到调用生效的流程大致就是这样了,本人也是正在学习中,由于水平有限可能有错误,还请包涵和指正!
最后致敬下ydwe团队,看代码后学习到非常多东西,感谢无私奉献!


IP属地:广东1楼2020-01-03 00:11回复


    IP属地:广东来自Android客户端2楼2020-01-03 00:52
    回复
      2026-02-13 17:11:41
      广告
      不感兴趣
      开通SVIP免广告
      神仙贴


      IP属地:四川来自Android客户端3楼2020-01-03 01:48
      收起回复
        留名,真正的技术贴,我一直以为lua和jass的集成是暴雪做的


        IP属地:广东来自Android客户端5楼2020-01-03 09:46
        回复
          cj 接口是 cdecl 约定的 c 函数 由调用者平衡栈,除了参数的转换,其他的跟一般 c/lua交互没什么区别


          IP属地:广东6楼2020-01-03 15:34
          回复
            写的很不错。不过没有c*背景的估计会一脸懵逼


            IP属地:福建来自Android客户端7楼2020-01-04 11:04
            回复
              一直不是很了解PE,得找个时间学一下


              IP属地:江苏来自Android客户端8楼2020-01-04 13:07
              回复
                会C就是好


                IP属地:北京来自Android客户端9楼2020-01-04 13:21
                回复
                  2026-02-13 17:05:41
                  广告
                  不感兴趣
                  开通SVIP免广告
                  膜拜大佬


                  IP属地:湖南来自Android客户端10楼2020-01-06 10:36
                  回复
                    差不多就是这样吧,你分析完也知道lua调用jass是有性能损耗的了。


                    IP属地:上海11楼2020-01-06 21:28
                    回复