借助动态代码生成技术在基于Webkit引擎的HTML5网页JS内调用易语言函数

80 篇文章 6 订阅
41 篇文章 2 订阅

作者:庄晓立(Liigo)

日期:2015年3月3日夜(2017年4月更新,详见文中和文末说明)

原创链接:http://blog.csdn.net/liigo/article/details/44045177

版权所有,转载请注明出处:http://blog.csdn.net/liigo


前两天我协助朋友解决了一个技术问题,在此稍作记录和总结。

具体来说,就是在使用基于Webkit引擎的封装组件wke的过程中,需要把一个易语言函数注册给JavaScript引擎,让它可以在网页里被调用(就像在网页里调用普通JavaScript函数一样)。如果能做到这一点,就基本实现了从JavaScript传递参数到易语言、易语言返回值给JavaScript的双向沟通机制,以后有广泛的应用空间。

在整体思路上,还是蛮简单的。因为wke已经提供了颇为直观的接口函数(虽然严重缺乏文档):

#define JS_CALL __fastcall
typedef jsValue (JS_CALL *jsNativeFunction) (jsExecState es);

WKE_API void jsBindFunction(const char* name, jsNativeFunction fn, unsigned int argCount);
WKE_API void jsBindGetter(const char* name, jsNativeFunction fn); /*get property*/
WKE_API void jsBindSetter(const char* name, jsNativeFunction fn); /*set property*/

WKE_API int jsArgCount(jsExecState es);
WKE_API jsType jsArgType(jsExecState es, int argIdx);
WKE_API jsValue jsArg(jsExecState es, int argIdx);
......

这里面最核心的函数是 jsBindFunction(),调用它就能注册一个新的JavaScript函数,只需提供函数名、实现回调函数、参数个数。在回调函数内部,通过 jsArgCount/jsArgType/jsArg 读取js传进来的参数,通过其他一些接口函数创建js值对象,都是一目了然的事情,这都不是事儿。

回调函数(fastcall)

首先卡在该回调函数的调用约定上:jsBindFunction的第二个参数,要求是 fastcall 调用约定的回调函数!可是易语言编译器根本就不支持编译生成fastcall调用约定的函数呀(仅支持stdcall)。fastcall 约定通过寄存器 ecx 和 edx 传递前两个参数,其余参数按照从右向左(从后往前)的顺序压栈,被调用者负责清理、平衡栈。这跟stdcall有一些类似但又明显不同。如果不管三七二十一盲目传递 stdcall 调用约定的回调函数进去,程序运行时非崩溃不可。

那怎么办呢?易语言编译器不支持fastcall,我们只好自食其力,纯手工生成二进制X86机器指令,人肉编译生成符合fastcall调用约定的回调函数。该函数声明的原型是:jsValue (__fastcall *jsNativeFunction) (jsExecState es),唯一个参数可从 ecx 寄存器中读取,没有入栈的参数,因而也不用平衡栈,直接 ret 就完事了。为了方便起见,我们引入两个易语言编写的函数:代理函数和用户函数,其中代理函数负责JS和易语言的类型转换,用户函数负责具体的执行逻辑,这两个函数毫无疑问都只能是stdcall调用约定(易语言编译器也不支持别的什么约定嘛)。下面设计我们的回调函数结构,以伪汇编代码来表示:

PUSH 用户函数地址
PUSH ecx
MOV eax, 代理函数地址
CALL eax
RET
这些伪汇编代码,要是用易语言写的话,其实就是一句话:返回(代理函数(es,用户函数))。(注:参数es是JavaScript引擎通过ecx寄存器传递进来的透明数据。)

易语言代码固然是简单,但因为编译器的限制,我们不能这么写。汇编代码稍微复杂一点,但我们仍然不能直接嵌入汇编(易语言编译器不支持)。只能手写机器码!把Intel指令集手册拿出来,查表,开工。既然是动态生成代码,当然需要先申请一块内存,然后把机器码填进去,然后把这块内存的首地址返回——这个内存的首地址也就是我们人肉编译生成的符合fastcall调用约定的回调函数的首地址。具体代码如下:



2017年4月Liigo更新:发现在Windows Server 2008系统下,“申请内存”申请到的内存区域不具有可执行权限,一尝试执行程序就崩溃了。

解决办法是将下面这一行代码:

函数体 = 申请内存 (32, 真)

替换为如下代码:

' Liigo 20170322
' 确保申请的内存具有可执行权限,否则在Windows Server 2008等系统下执行失败(access violation when executing)
' https://www.corelan.be/index.php/2010/06/16/exploit-writing-tutorial-part-10-chaining-dep-with-rop-the-rubikstm-cube/
函数体 = VirtualAlloc (0, 32, 十六进制 (“1000”), 十六进制 (“40”))  ' MEM_COMMIT(0x1000), PAGE_EXECUTE_READWRITE(0x40)。可不释放。
其中的VirtualAlloc是Windows API函数。


代理函数(stdcall)和用户函数(stdcall)

前面提到的代理函数,是一个很普通的易语言函数(stdcall),它负责解读JavaScript传递进来的参数,转换成易语言数据类型,转调易语言版的用户函数(也是stdcall),最后再把易语言用户函数的返回值转换为JavaScript类型后返回给JavaScript引擎。它接收两个参数,都是我们前面手工生成的回调函数传递进去的。代码如下:


代理函数的返回值是长整数型,也就是64位整数。根据 jsValue 的定义,它是64位指针,恰好可以用易语言的长整数表示。

JavaScript文本确定是UTF-8编码,转换到易语言文本之前,最好先执行编码转换(UTF-8 => GB18030),否则中文乱码。这一步骤非常简单,就作为课后作业吧。

我们完全可以改进这个代理函数,或者写另外一个代理函数,用于支持不同类型的用户函数(例如不同的参数类型和参数个数以及返回值类型)。

剩下的用户函数就更简单了,下面只是一个常规的示例(后面的测试代码就用到此函数):


把易语言函数注册为JavaScript函数

动态生成一个回调函数,作为参数传递给jsBindFunction即可:


函数调用次序总结

到了该总结一下的时候了:我们借助动态代码生成技术,在运行时生成一个符合fastcall调用约定的回调函数(jsNativeFunction),通过jsBindFunction将其注册到Javascript引擎,同时赋予其一个JavaScript函数名。网页脚本调用此JS函数时,回调函数被调用,进而回调函数又调用了代理函数,代理函数又调用了用户函数,用户函数返回后,返回值又被逐层返回给JS引擎。

测试代码

首先注册一个测试用的JS函数,易语言代码:注册JS函数("plus1",用户函数示例)。
再先来一段HTML,加载到浏览器中:
<a href='#' οnclick="document.getElementById('result').value=plus1('liigo');">link</a>
<p>
<textarea rows='6' cols='36' id='result'>hello</textarea>
当点击网页中的链接时,之前注册的JS函数 plus1 将被执行,进而易语言函数 用户函数示例 被调用。易函数返回的文本,成了 plus1 的返回值,最终输出到网页内的编辑框中。如果编辑框中文本显示为“liigo hohoho”,说明测试成功。

写在最后的思考

能否将前面的实现方案用易语言内置函数 “置入代码” 替换?我(Liigo)想,至少有两个阻力妨碍我们在此应用置入代码:1、置入代码只能作用于已经存在的函数,而不能运行时动态生成新的函数;2、置入代码是编译时行为,置入的代码不能包含可变量(例如上文的 CALL eax 恐怕就行不通)。勉强应用置入代码,也不是不行,只是会让每一个用户函数都非常复杂,既包含了置入代码,又包含了JS和易语言的类型转换,还包含了业务逻辑,没有任何封装性可言,易用性约等于零。


全文完。谢谢收看!知道我是谁吗?







































大名鼎鼎的御前四品带刀护士!



----------


2017年4月Liigo更新:

碰巧了,前一阵子我又用到WKE/Webkit。找了一圈也没找到之前备份的源代码,不知道丢哪里去了;只找到本文,照着文中图片上的代码,抄了一遍,你别说,还真管用。顺便说句题外话,话说当年,两年前,2015年3月,我正式发表本文没过几分钟,就有易友照着图片把代码打出来了。说回正题,这次重抄一遍代码,还发现了一个问题并解决掉了,详见文中“回调函数(fastcall)”一节。


  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 14
    评论
第一期使用命令介绍: 类: (CefApp): 与进程,命令行参数,代理,资源管理相关的回调类 (CefBrowserProcessHandler): 用于接收进程相关的回调通知。 (CefClient): 回调管理类,主要是用于向浏览器反回我们需要接管哪些功能的类。 (CefLifeSpanHandler):浏览器的运行管理类,包含当浏览器创建完成之后,浏览器被关闭等通知 (CefMainArgs): 数据类,用于设置当前应用实例句柄的。 (CefSettings): 数据类,用于设置一些浏览器整体的基本信息 (CefWindowInfo): 数据类,用于设置一些浏览器的窗口信息 (CefBrowserSettings): 数据类,用于设置一些浏览器的基本信息 用到的类命令: 返回值 (CefBrowserProcessHandler) = (CefApp).GetBrowserProcessHandler()  '获取一个用于管理浏览器进程的类 返回值 空= (CefBrowserProcessHandler).OnContextInitialized()  '回调通知函数,告诉我们浏览器已经准备就绪了。 返回值 (CefLifeSpanHandler) = (CefClient).GetLifeSpanHandler()  '向浏览器返回我们用于接管浏览器进程的类 返回值 空 = (CefLifeSpanHandler).OnAfterCreated()  '回调通知函数,用于告诉我们,当前有一个新的浏览器创建好了 返回值 空 = (CefLifeSpanHandler).DoClose()  '回调通知函数,貌似是表示所有浏览器都关闭之后,Cef上有一大段注释,但是TM始终看不明白... 返回值 空 = (CefLifeSpanHandler).OnBeforeClose()  '回调通知函数,用于告诉我们,当前有一个浏览器被关闭了 返回值 空 = CefMainArgs.Load()  '数据类函数,用于设置当前当前应用的实例句柄 返回值 空 = CefSettings.SetAsSingleProcess()  '数据类函数,是否使用单进程运行浏览器, 1.单进程运行 0.多进程运行。默认是以多进程运行的。 返回值 空 = CefSettings.SetAsNoSandbox()  '数据类函数,是否关闭沙盘功能 返回值 空 = CefSettings.SetAsRemoteDebuggingPort()  '数据类函数,设置远程调试端口 返回值 空 = CefWindowInfo.SetAsChild()  '数据类函数,设置浏览器窗口为子窗口 通用类命令: (All).AddRef()  '给这个函数所属的类增加一次引用计数 (All).Release()  '给这个函数所属的类释放一次引用计数 (All).HasOneRef()  '判断当前这个类是不是第一次被引用 (All).Wrap()  '实际上《Hello WebKit》框架的类都是以一种接近于C++类的存在,为了能给浏览器使用,我们必须要将这个类转换为近似于C的类。这个函数就有这样的作用 (All).Unwrap()  '从C类中取回我们的C++类 (All).ToCpp__() '导入或取出由浏览器提供的类指针或者数据指针 通用命令: CefBrowserHostCreateBrowserSync()  '创建一个新的浏览器,成功返回浏览器类CefBrowser. CefExecuteProcess()  '初始化浏览器进程 CefInitialize()  '全初始化,该函数执行完成之后,(CefBrowserProcessHandler).OnContextInitialized() 将收到通知 CefRunMessageLoop()  '浏览器进程消息循环 CefShutdown()  '浏览器进程结束 CefQuitMessageLoop()  '向所有(多进程下)浏览器进程发送结束通知 REQUIRE_UI_THREAD()  '调试函数,用于检查执行到该函数位置的线程/进程是否为UI线程/进程,如果不是将被中断下来 CEF_BROWSER_RELEASE()  '释放一次浏览器的引用计数 第二期使用命令介绍: (CefDisplayHandler): 与浏览器状态显示相关的类 (CefBrowser): 浏览器类,用于控制或者取得浏览器的相关信息,最常用的类 (CefBrowserHost): 浏览器窗口类,用于控制或者取得浏览器窗口的相关信息,最常用的类 (CefFrame): 浏览器框架类,用于控制或者取得浏览器框架的相关信息,最
源码介绍: 1、HTML是目前在用户体验、界面舒适度最先进的语言 2、HTML所有标签都是容器,CSS调试效果方便 3、最重要的一点,让做界面的去界面,写程序的去写程序吧,界面与程序分享得更彻底 4、支持js、jq和易语言交互! 5、窗口尺寸自适应; 6、支持FLASH; 7、网页文件可以放在服务器上(如果你有虚拟机或者云主机的话),也可以加密打包后,内存加载;还可以放在资源表里;防止界面代码泄露; 例子中,做了简单的交互示例。并无实际功能! 本例程仅演示界面功能,实际开发需添加更多功能代码; (调用了开源的wke模块) 什么是wkeWebKit 是一个开源的浏览器引擎,同时WebKit 也是苹果Mac OS X 系统引擎框架版本的名称,主要用于Safari,Dashboard,Mail 和其他一些Mac OS X 程序。WebKit 前身是 KDE 小组的 KHTML,WebKit 所包含的 WebCore 排版引擎JSCore 引擎来自于 KDE 的 KHTML 和 KJS,当年苹果比较了 Gecko 和 KHTML 后,仍然选择了后者,就因为它拥有清晰的源码结构、极快的渲染速度。apple将 KHTML 发扬光大,推出了装备 KHTML 改进型 WebKit 引擎的浏览器 Safari。Safari, Google Chrome,傲游3,猎豹浏览器,百度浏览器 opera浏览器 等都是基于 Webkit 开发。,Webkit作为一款优秀的浏览器内核,它众多优秀的特性引起业内的的广泛关注。尤其是近来,google的加入更是让Webkit有所升温,从 Goole Chrome浏览器, Goole Anroid手机操作系统内置浏览器均采用Webkit作为内核。
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值