实现一个 JavaScriptCore 的 debugger —— iOS 篇
作者: 发布于:

JSC 的 debugger 是个神奇的东西,在网上资料甚少,几乎完全搜索不到,尤其是 iOS 上,OC 接口没有暴露任何 debugger 信息。

不过好在 JSC 是开源的,通过分析源代码可以找到 JSC::Debugger 这个抽象类,我们继承这个抽象类,然后实现掉虚函数,创建实例并且把它挂载到 global object 即可开启 debug 能力了。

思路是简单的,在 iOS 设备上,我们要面临的另一个问题是它的 JavaScriptCore 本身是以 Framework 的形式而非源代码形式提供的,所以我们只有公开的 OC 头文件和静态库文件。

所以要想使用 debugger,我们需要:

  1. 编译时使用私有的头文件

  • 确保头文件的版本跟 framework 一致
  • 确保编译选项跟 framework 一致

  1. 链接时链接 framwork 中的方法

如何解决呢?步骤如下:

  1. 查看 framework 中的 JSC 版本
  1. 根据 JSC 版本,找到对应的源代码
  1. 构建 JSC 获取私有头文件
  1. 建立新项目,引入私有 JSC 头文件
  1. 调整宏和编译选项
  1. 编写代码

查看 framework 中的 JSC 版本

framework 是一个文件夹(在 XCode 中右键即可打开),可以从tbd 文件中找到系统中库的路径(一般是 /System/Library/Frameworks/JavaScriptCore.framework/ ),然后从 version.plist 中找到当前版本。

在我的 XCode9.2中,找到的版本是 604.4.7.1.3。

下载源代码

https://svn.webkit.org/repository/webkit/tags 可以找到对应的源代码。

注意一般 opensource.apple.com 中找不到对应版本。

我们并不需要整个 webkit 代码,所以只要下载 source 目录下的 bmalloc, WTF 和 JavaScriptCore 三个项目就够了。

构建 JSC

首先我们需要建立一个 workspace,然后把三个项目文件拖进 workspace。

依次构建 bmalloc、WTF 和 JavaScriptCore 三个项目即可。

如果配置正确,构建 JSC 应该不会遇到什么困难。

我们只需要构建好的头文件,所以不需要选择 iOS 设备,使用默认的 mac 作为目标就好了。

建立新项目

接下来我们要建立一个 Debugger 项目,随便叫什么名字,选择 iOS 项目。

我们需要调整编译选项:

  • other C++ flags: -std=c++14
  • enable C++ runtime Types: No
  • system header search path: $(PRODUCT_NAME)/
  • Processor Macros:(太多了,建议直接到项目文件源代码里修改)
    ENABLE_3D_TRANSFORMS,
    ENABLE_ACCELERATED_OVERFLOW_SCROLLING,
    ENABLE_APPLE_PAY,
    ENABLE_APPLE_PAY_SESSION_V3,
    ENABLE_ATTACHMENT_ELEMENT,
    ENABLE_AVF_CAPTIONS,
    ENABLE_CACHE_PARTITIONING,
    ENABLE_CANVAS_PATH,
    ENABLE_CANVAS_PROXY,
    ENABLE_CHANNEL_MESSAGING,
    ENABLE_CONTENT_FILTERING,
    ENABLE_CSS_ANIMATIONS_LEVEL_2,
    ENABLE_CSS_BOX_DECORATION_BREAK,
    ENABLE_CSS_COMPOSITING,
    ENABLE_CSS_DEVICE_ADAPTATION,
    ENABLE_CSS_IMAGE_ORIENTATION,
    ENABLE_CSS_IMAGE_RESOLUTION,
    ENABLE_CSS_REGIONS,
    ENABLE_CSS_SCROLL_SNAP,
    ENABLE_CSS_SELECTORS_LEVEL4,
    ENABLE_CSS_TRAILING_WORD,
    ENABLE_CSS3_TEXT,
    ENABLE_CURSOR_VISIBILITY,
    ENABLE_CUSTOM_SCHEME_HANDLER,
    ENABLE_DASHBOARD_SUPPORT,
    ENABLE_DATA_INTERACTION,
    ENABLE_DATA_TRANSFER_ITEMS,
    ENABLE_DATACUE_VALUE,
    ENABLE_DATALIST_ELEMENT,
    ENABLE_DEVICE_ORIENTATION,
    ENABLE_DRAG_SUPPORT,
    ENABLE_ENCRYPTED_MEDIA,
    ENABLE_FETCH_API,
    ENABLE_FILTERS_LEVEL_2,
    ENABLE_FTL_JIT,
    ENABLE_FULLSCREEN_API,
    ENABLE_GAMEPAD_DEPRECATED,
    ENABLE_GAMEPAD,
    ENABLE_GEOLOCATION,
    ENABLE_ICONDATABASE,
    ENABLE_INDEXED_DATABASE_IN_WORKERS,
    ENABLE_INDEXED_DATABASE,
    ENABLE_INPUT_TYPE_COLOR_POPOVER,
    ENABLE_INPUT_TYPE_COLOR,
    ENABLE_INPUT_TYPE_DATE,
    ENABLE_INPUT_TYPE_DATETIME_INCOMPLETE,
    ENABLE_INPUT_TYPE_DATETIMELOCAL,
    ENABLE_INPUT_TYPE_MONTH,
    ENABLE_INPUT_TYPE_TIME,
    ENABLE_INPUT_TYPE_WEEK,
    ENABLE_INTERSECTION_OBSERVER,
    ENABLE_INTL,
    ENABLE_IOS_GESTURE_EVENTS,
    ENABLE_IOS_TOUCH_EVENTS,
    ENABLE_JIT,
    ENABLE_KEYBOARD_KEY_ATTRIBUTE,
    ENABLE_KEYBOARD_CODE_ATTRIBUTE,
    ENABLE_LEGACY_CSS_VENDOR_PREFIXES,
    ENABLE_LEGACY_ENCRYPTED_MEDIA,
    ENABLE_LEGACY_VENDOR_PREFIXES,
    ENABLE_LETTERPRESS,
    ENABLE_LINK_PREFETCH,
    ENABLE_MAC_GESTURE_EVENTS,
    ENABLE_MATHML,
    ENABLE_MEDIA_CAPTURE,
    ENABLE_MEDIA_CONTROLS_SCRIPT,
    ENABLE_MEDIA_SESSION,
    ENABLE_MEDIA_SOURCE,
    ENABLE_MEDIA_STATISTICS,
    ENABLE_MEDIA_STREAM,
    ENABLE_METER_ELEMENT,
    ENABLE_MHTML,
    ENABLE_MOUSE_CURSOR_SCALE,
    ENABLE_NAVIGATOR_CONTENT_UTILS,
    ENABLE_NAVIGATOR_STANDALONE,
    ENABLE_NOTIFICATIONS,
    ENABLE_PDFKIT_PLUGIN,
    ENABLE_POINTER_LOCK,
    ENABLE_PROXIMITY_EVENTS,
    ENABLE_PUBLIC_SUFFIX_LIST,
    ENABLE_QUOTA,
    ENABLE_REMOTE_INSPECTOR,
    ENABLE_REQUEST_AUTOCOMPLETE,
    ENABLE_RESOLUTION_MEDIA_QUERY,
    ENABLE_RESOURCE_USAGE,
    ENABLE_RUBBER_BANDING,
    ENABLE_SERVICE_CONTROLS,
    ENABLE_SPEECH_SYNTHESIS,
    ENABLE_STREAMS_API,
    ENABLE_SUBTLE_CRYPTO,
    ENABLE_SVG_FONTS,
    ENABLE_TELEPHONE_NUMBER_DETECTION,
    ENABLE_TEXT_AUTOSIZING,
    ENABLE_TOUCH_EVENTS,
    ENABLE_TOUCH_ICON_LOADING,
    ENABLE_USERSELECT_ALL,
    ENABLE_VARIATION_FONTS,
    ENABLE_VIDEO_PRESENTATION_MODE,
    ENABLE_MAC_VIDEO_TOOLBOX,
    ENABLE_VIDEO_TRACK,
    ENABLE_VIDEO,
    ENABLE_VIEW_MODE_CSS_MEDIA,
    ENABLE_WEB_ANIMATIONS,
    ENABLE_WEB_AUDIO,
    ENABLE_WEB_RTC,
    ENABLE_WEB_SOCKETS,
    ENABLE_WEB_TIMING,
    ENABLE_WEBGL,
    ENABLE_WEBGL2,
    ENABLE_WEBGPU,
    ENABLE_WIRELESS_PLAYBACK_TARGET,
    ENABLE_XSLTFAST_JIT_PERMISSIONS

然后我们打开构建好的 JSC 项目目标, 复制其中 PrivateHeaders 目录,到项目目录的 JavaScriptCore 目录。

再打开 WTF 项目目标, 复制目录下 /usr/local/include/wtf

接下来,我们需要对源代码做一下小修改,因为系统的 JSC 是在非 debug 模式下编译的,所以我们强行把头文件中跟 debug 相关的代码改成非 debug 模式:

JavaScriptCore/HandleStack.h

所有 #ifdef NDEBUG

WTF/hashtable.h

#ifdef NDEBUG
#define CHECK_HASHTABLE_ITERATORS 0
#define CHECK_HASHTABLE_USE_AFTER_DESTRUCTION 0
#else
#define CHECK_HASHTABLE_ITERATORS 0
#define CHECK_HASHTABLE_USE_AFTER_DESTRUCTION 0
#endif

编写代码

代码必须使用 .mm 文件。

我们需要在项目的 build phases 中加入 JavaScriptCore.framework。


#import <JavaScriptCore/JavaScriptCore.h>

#import "JavaScriptCore/HeapInlines.h"
#import "JavaScriptCore/HeapCellInlines.h"
#import "JavaScriptCore/APICast.h"
#import "JavaScriptCore/Debugger.h"
#import "JavaScriptCore/SourceProvider.h"
#import "JavaScriptCore/JSRunLoopTimer.h"
#import "JavaScriptCore/JSVirtualMachineInternal.h"

class MyDebugger: public JSC::Debugger {
public:
    MyDebugger(JSC::VM& vm) : JSC::Debugger::Debugger(vm){

    }
    
    
    virtual ~MyDebugger(){
        JSC::Debugger::~Debugger();
    }

    /*virtual void recompileAllJSFunctions() {
        //JSC::Debugger::recompileAllJSFunctions();
    }*/
    virtual void sourceParsed(JSC::ExecState* state, JSC::SourceProvider* sourceProvider, int errorLineNumber, const WTF::String& errorMessage) {
        //StringView sourceString = sourceProvider->source();
        
        //NSLog(@"sourceParsed:%@", (NSString*)sourceString.toString());
        NSLog(@"sourceParsed");
        return;
    };
    virtual void handleBreakpointHit(JSC::JSGlobalObject*, const JSC::Breakpoint&) {
        NSLog(@"handleBreakpointHit");
    }
    virtual void handleExceptionInBreakpointCondition(JSC::ExecState*, JSC::Exception*) const {
        NSLog(@"handleExceptionInBreakpointCondition");
    }
    virtual void handlePause(JSC::JSGlobalObject* globalObject, ReasonForPause reason) {
        NSLog(@"handlePause");
    }
    virtual void notifyDoneProcessingDebuggerEvents() {
        NSLog(@"notifyDoneProcessingDebuggerEvents");
    }
};

使用示例:

    JSContext *jsContext = [[JSContext alloc] init];

    JSGlobalContextRef globalContext = [jsContext JSGlobalContextRef];

    JSC::ExecState* es = toJS(globalContext);
    JSC::JSGlobalObject* globalObject = es->vmEntryGlobalObject();

    MyDebugger* debugger = new MyDebugger(globalObject->vm());
    globalObject->setDebugger(static_cast<JSC::Debugger*>(debugger));
    debugger->setPauseOnNextStatement(TRUE);
    
    globalObject->vm().heap.acquireAccess();
    debugger->activateBreakpoints();
    globalObject->vm().heap.releaseAccess();

    [jsContext evaluateScript:@"debugger;"];

题图:https://unsplash.com/photos/Sf5Q7Ljjf58 By @Katerina Pavlickova