目录
- 常见问题(General Questions)
- Cycript 控制台(Cycript Console)
- JavaScript 解析器(JavaScript Parser)
- CommonJS 模块(CommonJS Modules)
- C 类型和指针(C Types and Pointers)
- C 结构体和数组(C Structs and Arrays)
- Objective-C 基础(Objective-C Basics)
- Objective-C 高级(Objective-C Advanced)
- Java 初始预览版(Java Initial Preview)
- 对象查询(Object Queries)
- 进程注入(Process Injection)
- iOS 静态库(iOS Static Library)
- Cydia Substrate
- 接下来是什么?(What Next ?)
常见问题(General Questions)
人们在使用 Cycript 之前可能遇到的问题的答案
-
什么是 Cycript?(What is Cycript ?)
Cycript 是 ECMAScript some-6、Objective-C++ 和 Java 的混合体。它被实现为 Cycript-to-JavaScript 编译器,并使用(未经修改的)JavaScriptCore 作为其虚拟机。它专注于通过采用其他语言的语法和语义来提供流利的语言交互接口(FFI,Foreign Function Interface),而不是将其他语言视为二等公民
Cycript 的主要用户目前是在 iOS 上做逆向工程的人。Cycript 具有高度交互的控制台,具有实时的语法高亮显示和语法辅助补全(通过
Tab
键),甚至可以使用 Cydia Substrate 注入到正在运行的进程中(类似于调试器)。这使其成为逆向工程中探索 iOS 应用程序的理想工具然而,Cycript 被专门设计为一个编程环境,并且为这个用例维护很少包袱(如果有的话)。
node.js
中的许多模块都可以加载到 Cycript 中,同时 Cycript 还可以直接访问为 Objective-C 和 Java 编写的库。因此,Cycript 作为一门脚本语言能工作得非常好 -
为什么要开发 Cycript?(Why develop Cycript ?)
我的背景是研究编程语言、编译器和运行时代码生成。我在加州大学圣巴巴拉分校教一门课,从比较语言学的角度来研究机器语言。Cycript 本质上是编程语言运行时环境的跨界同人小说
Cycript 始于 2007 年,作为 Objective-C 和 Java 之间的桥梁,称为 JOCStrap(Java/Objective-C BootStrap)。那个项目有点有趣,但令人惊奇的是,当我意识到可以使用 Rhino(JavaScript 的 Java 实现)来获得交互式控制台时:可以说,这就是我学会使用
Cocoa/UIKit
的方式直到 2009 年,在 360|iDev 时,我才最终拆分了 JOCStrap 并将其重构为 Cycript。多年来,我为 Cycript 添加了许多功能和改进,并且通常是在 360|iDev上演示 Cycript 的新版本(我认为这是 Cycript 的家会议)。其结果是,Cycript 成为我与 gdb 一起使用的一个功能强大的调试工具
-
Cycript 的发音(Pronunciation)
我使用(双 S)或(长 S)来发音 Cycript。结果听起来有点像卡通片中典型的蛇:sssscript。虽然我怀疑不是所有人都会这样发音,但是我有自己的希望。我还经常将 Cycript 用作跟库和应用程序的自动化和注入相关的动词,这强调了 Cycript 作为脚本语言的强大功能
-
Cycript 的稳定性和状态(Stability and Status)
Cycript 能很好地工作。虽然我一直在不断改进核心语言和库,但是作为调试工具,这并不是一个问题。当我确实进行更改时,它们往往是桥接很少使用的方面,或者让 Cycript 的日常使用变得更加方便。作为一个静态库嵌入,这也不是一个问题
然而,我不建议尝试将 Cycript 用作通用编程语言。坦率地说,Cycript 在这方面并不是很擅长:我曾经尝试在 Cycript 中编写一个普通的 iPhone 应用程序,结果实际上比使用 Objective-C++ 编写的要稍微冗长一些(由于 C++11 的自动特性)。建议使用 Objective-C++ 来开发应用程序
-
如何获取帮助(Getting Help)
我们鼓励正在寻求 Cycript 帮助的用户加入我们 IRC 服务器 irc.saurik.com 上的 #cycript 频道。那里往往有很多使用 Cycript 的人,而且他们通常对于回答人们关于如何使用 Cycript 的问题感兴趣。如果你没有 IRC 客户端,则可以尝试使用 Mibbit
(请理解 IRC 涉及与其他用户的实时聊天。如果你在 IRC 上提问并且似乎没有人在附近,则你应该至少在线几个小时:因为每个人都有不同的日常安排,而且通常都在不同的时区。人们也倾向于在做其他事情的时候开着 IRC 窗口,因此不会立即看到新内容,而是偶尔检查一下)
Cycript 控制台(Cycript Console)
如何运行 Cycript 控制台,以及所期待的事情
-
$ ./cycript
使用 Cycript 时要做的第一件事就是让 Cycript 运行起来比较舒服。如果你是为 macOS 下载了 Cycript,则你可能已经将 Cycript 解压到了某个文件夹下:打开一个终端(最好是 iTerm2),进入到解压的文件夹,然后运行
./cycript
。如果你使用的是 iOS,则 Cycript 应该已经配置在你的环境变量PATH
上了:你只需要运行cycript
iPhone:~$ cycript cy#
cy#
提示符是一个 JavaScript 控制台。你输入的所有内容都将由 JavaScriptCore 运行,而 JavaScriptCore 是 Apple 对 Safari 中所使用的 JavaScript 语言的实现。在你输入时,你的命令将使用 Cycript 的词法分析器进行语法高亮显示。如果你在输入命令时出现了错误,则你会得到一个语法错误的提示。你可以使用Ctrl + C
取消和Ctrl + D
退出cy# var a = 3 cy# Math.pow(a, 7) 2187 cy# var a 3 ..........^ | syntax error, unexpected NumericLiteral, expecting ; cy# function f(a) { a = a + 5; return ^C cy# ^D iPhone:~$ cycript
Cycript 的控制台是使用
readline
实现的(是的,即使是在 macOS 上,Cycript 也有自己的readline
副本)。如果你对readline
不太了解,则我强烈建议你进一步了解它:readline
有许多键盘快捷键,允许你非常快速地操作命令。readline
还提供历史搜索(Ctrl + R
)。hcg 注:readline
是一个用于实现命令行交互界面(例如,控制台)的库需要特别注意的是,编辑多行命令的方式。我花了几个小时与 Brian Fox(
readline
的开发者)一起研究如何使用未修改的readline
库副本来获取我想要的一些语义(请注意,在多行代码块上下文中,行首的制表符Tab
被视为缩进) -
控制台行为(Console Behavior)
你输入的每个命令的结果的值都存储在一个名为
_
的变量中,你可以在下一个命令中使用该变量(现在,该变量存储在 JavaScript VM 的全局范围内,但我打算在未来的版本中按 Cycript 控制台存储该变量,所以请不要尝试依赖在连接到同一 JavaScript VM 的多个控制台之间共享的_
)cy# 5 + 9 14 cy# _ 14
-
CYON 和 ? 命令(CYON and ? commands)
有时候,你想要告诉 Cycript 控制台一些事情,而不是将输入作为 JavaScript 进行解析和运行。这些命令以问号
?
开头。最令人感兴趣的是?exit
,这是一个可以在不使用快捷键Ctrl + D
的情况下用于退出 Cycript 控制台的显式命令。你还可以使用?syntax
切换语法的高亮显示cy# ?syntax syntax == false cy# ?exit iPhone:~$ cycript
例如,每当 Cycript 控制台呈现一个值时,它都会使用 CYON,有些人(不是我)声称它代表 Cycript Object Notation。一般的想法是,Cycript 控制台所呈现的所有内容本身就都是可以由该语言解析的代码,并希望能够生成类似的结果。你可以在运行时使用
.toCYON()
获取 CYONcy# "hello".toCYON() '"hello"' cy# [1,2].toCYON() "[1,2]" cy# new Number(3+10) new Number(13) cy# (function() { throw new Error(); })() throw new Error("")
此输出有时可能是混乱的,并且通常是非常小的。作为一项实验性功能,你可以告诉 Cycript 重新解析输出,然后以友好的方式打印结果。这是通过使用
?reparse
切换此功能来完成的(如果 CYON 无效,则此功能只会打印原始文本。因此除非在以友好的方式打印结果时出现错误,否则它应该是无害的)cy# ({a: 10, b: 15}) {a:10,b:15} cy# ?reparse reparse == true cy# ({a: 10, b: 15}) { a: 10, b: 15, }
-
制表符代码补全(Tab Completion)
Cycript 的主要优势之一是其复杂的制表符代码补全机制。Cycript 不是在标记或字符串的级别上实现制表符代码补全,而是将制表符代码补全作为语言解析器和运行时环境的一部分来实现的。这意味着你通常可以在远离其调用站点的上下文中,或在运行时创建的对象上,使用制表符补全代码
cy# ({field: 123}).fi<Tab>
在上面这种情况下,我们已经输入了一个对象字面量
({field: 123})
,并希望能够从其字段fi
中通过制表符补全代码。在大多数其他的开发环境中,这要么失败,要么只完成对关键字finally
的补全。相比之下,Cycript 会执行对象字面量并在运行时检查它,获取其属性列表(包括来自 Object 的prototype
),并补全该字段
JavaScript 解析器(JavaScript Parser)
Cycript 并不依赖于 JavaScriptCore 作为解析器
-
JavaScript 编译器(JavaScript Compiler)
在使用 Cycript 时必须要理解的一件事情是:Cycript 不会简单地将你的命令视为输入,并将它们直接传递给 JavaScriptCore。相反,Cycript 包含了 JavaScript 语法的完整实现。这不仅改进了 Cycript 对制表符代码补全的实现,而且允许 Cycript 添加新的语法
例如,JavaScript 一个令人沮丧的方面是,如果对数组使用
for-in
循环,则不会迭代数组的元素。相反地,for-in
循环总是迭代对象的属性名称,对于数组来说for-in
循环迭代的是一个字符串序列,每一个字符串对应数组的一个有效索引。然后,你必须自己手动查找数组的元素cy# var before = [2, 4, 5]; cy# var after = []; for (var i in before) after.push(i); after ["0","1","2"] cy# var after = []; for (var i in before) after.push(before[i]); after [2, 4, 5]
因为这是一个常见的问题,所以 ECMAScript 6 包含了一个新的
for-of
特性。Cycript 将此for-of
特性作为其语法的一部分实现,并将此for-of
特性转换为可在 JavaScriptCore 上执行的旧版 JavaScript 代码。你可以使用?debug
查看 Cycript 生成的内容(如果你怀疑存在错误,则特别方便)cy# var before = [2, 4, 5]; cy# var after = []; cy# ?debug debug == true cy# for (var i of before) after.push(i) cy= var $cy3,$cy2,i;{$cy3=before;for($cy2 in $cy3){i=$cy3[$cy2];after.push(i)}} cy# after cy= after [2,4,5]
Cycript 中存在的语法扩展将在本手册的后续部分中讨论,因为它们之间是相关的。这些语法扩展中的大多数都添加了类似于 Objective-C++ 的语法,允许开发者将代码尽可能多地直接复制/粘贴到 Cycript 控制台中。其他特性是从 JavaScript 1.6+ 和 ECMAScript 5+ 添加的
-
JavaScript 缩小器(JavaScript Minifier)
还应该注意的是,Cycript 的最初目标之一是成为一个 JavaScript Minifier。Cycript 在这方面实际上是(或至少是)相当有竞争力的,因为 Cycript 提供了:比雅虎更好的输出、对微软有竞争力的输出。当 Google 的 Closure Compiler 关闭了具有高风险的高级功能时,Cycript 甚至比 Google 的 Closure Compiler 更好
由于这段历史的部分原因,你从 Cycript 获取的输出往往与你提供给 Cycript 的输入完全不同,并使用了许多技巧来减少所产生的代码大小。请理解,Cycript 的主要目标是:gzip 之后的大小,所以 Cycript 生成的一些东西(var list)虽然看起来很冗长,但是压缩起来很小
不过,老实说,Google Closure Compiler 的发布(发生在我做这个 JavaScript Minifier 的时候)让我停止了改进这些特性的工作,所以有时输出会不必要地定义了一些额外的变量、添加了一组额外的括号,或者没有注意到变量名应该缩短。也许有一天,这些特性还会得到改进
cy# (function() { var v = "\"" + ((0 + (1)) * (2 * 3)) + "hello" + global; return v; }) cy= (function(){var e;e='"6hello'+global;return e})
(事实上,当我致力于添加 ECMAScript 6 的支持时,我对此更加松懈,目前那些嵌套的括号会导致优化障碍,使该示例不再折叠为
'"6hello'
。我认为这是一个错误,而不是一个新常态)如果你只想查看 Cycript 将为给定的文件输出什么,则你可以使用
-c
标志将脚本编译为标准输出。我目前实际上不建议将其用于通用网站(尽管我过去曾这样做过)。如果你想要一个工业级的压缩编译器,则我目前推荐 Google Closure CompileriPhone:~$ echo '5 + 6' >file.cy iPhone:~$ cycript -c file.cy; echo 11 iPhone:~$
-
ECMAScript 6 的状态(ECMAScript 6 Status)
我最近的主要关注点之一是对 ECMAScript 6 的支持,我非常感谢 Yehuda Katz(在 ECMAScript 6 标准化委员会中)与我一起处理规范中的一些极端情况。目前我认为 Cycript 的语法完全符合 ECMAScript 6
然而,这并不是说所有的特性都实现了!举个简单的例子,Cycript 还不支持生成器(generator)。Cycript 也不处理解构绑定(
destructuring bind
)(如果在let binding
中使用notice
)。如果你尝试使用此语法,则 Cycript 将抛出一个语法错误(这些特性未来将被支持,只是目前还不支持)cy# var a = function *() {} ............^^^^^^^^^^^^^^^ | unimplemented feature
也就是说,不要假设什么都行不通。我只是说明了 Cycript 目前还不支持的主要部分,以提供对 Cycript 的实际预期;但是有很多事情确实是有效的,特别是我花了大量的时间在其他解析器出错的事情上(例如,标识符和字符串字面量的 Unicode 合规性)
cy# class A { constructor(value) { this.hello = value; } world() { return this.hello + 3; } } function A(t) { var e; e = this; e.hello = t; } cy# class B extends A { constructor(value) { super(value); this.test = 6; } world() { return super.world() + 10; } } function B(t) { var e; e = this; i.bind(e)(t); e.test = 6; } cy# var b = new B(10) new B{hello:10,test:6} cy# b.world() 23
CommonJS 模块(CommonJS Modules)
许多 node.js
库可以使用 require()
导入
-
Node.js 核心库(Node.js Core Libraries)
Cycript 附带了全套的
node.js
核心库,虽然它现在与libuv
链接,但是它还没有提供内部绑定。这意味着这个兼容性功能是:命中或丢失,并且经常会丢失。Cycript 的下一个主要步骤之一将是改进这个故事(可能通过在 Cycript 而不是 C 中实现这些绑定)cy# var path = require('path') cy# path.relative("/a/b/c", "/a/d") "../../d" cy# var util = require('util') cy# util.format('%s:%s', 'foo', 'bar', 'baz'); "foo:bar baz"
-
ECMAScript 6 模块语法(ECMAScript 6 Module Syntax)
Cycript 为导入提供了 ECMAScript 6 模块语法(导出的语法尚未翻译)。这适用于所有现有的
CommonJS
模块(请注意,我目前与babel.js
使用的 horrible hack 不兼容。解决他们问题的正确方法是让require()
设置module.default = module
,或者依赖* as
,而不是 fork 模块生态系统)cy# import {format} from 'util'; cy= var $cy1,format;{$cy1=require("util");format=$cy1.format} cy# import * as util from 'util'; cy= var $cy1,util;{$cy1=require("util");util=$cy1}
C 类型和指针(C Types and Pointers)
如何使用 Cycript 的语法与用 C 编写的代码进行交互
-
类型对象(Type Objects)
用 C 编写的代码非常关心类型:除非你知道变量或函数的完整类型,否则你无法访问变量或调用函数。因此,Cycript 必须拥有这些类型的强大模型,并且 Cycript 的目标是通过对 JavaScript 语法的扩展,让 C 程序员可以轻松快速地上手使用 Cycript 的类型支持
Cycript 中的类型本身就是运行时的值,是类对象
Type
的实例。C 中的基本原生类型已提供给了 Cycript。要构建更复杂的类型,可以通过typedef
关键字直接使用 C 类型编码语法(在 JavaScript 惯例中,它可以用作表达式,尽管只能在括号中使用)cy# int (typedef int) cy# typedef int* intp; cy# (typedef const intp) (typedef int* const) cy# typedef int (*callback)(double, int[3]); callback (typedef int (*)(double, int [3]))
-
指针和强转(Pointers and Casting)
真正使用 C 意味着使用指针。Cycript 在 JavaScript 中将指针建模为
Pointer
类型的对象。要分配内存,你可以在Type
类型的对象上使用 JavaScript 的new
运算符,以执行copy-construction
(类似于 C++ 的拷贝构造)。在 CYON 中,指针被表示为其值的地址。内存使用垃圾回收(抱歉)你可以使用
typeid
获取任何值的运行时类型,typeid
会返回一个Type
类型的对象。你甚至可以在不是指针的变量上使用typeid
,尽管只有当值的类型被认为是明确的时,你才会得到结果(你发现此问题的主要地方是 JavaScript 中的数字:它们没有typeid
结果)cy# typeid(3) cy# p = new int(7) &7 cy# typeid(p) (typedef int *) cy# typeid("hello") (typedef char const*)
要使用指针指向的值,可以使用 C 中的间接语法
*
。还可以使用Type
类型的对象作为函数在不同类型的指针之间进行强转。如果需要指针表示的地址,则可以将其强制转换为uintptr_t
(或通常使用.valueOf()
)。可以使用适当的Type
类型的对象将数字强转回指针cy# p = new int &0 cy# typeid(*p) (typedef int) cy# *p = 0x40000000|5<<21 1084227584 cy# *(typedef float *)(p) 5 cy# uintptr_t(p) (typedef unsigned long)(4298159072) cy# *@encode(int *)(4298159072) 1084227584
-
函数指针(Function Pointers)
要从 C 中调用函数,你需要知道函数的地址。虽然 Cycript 已经为你提供了许多常见的 Unix 函数(我敢说,几乎所有),但是如果你需要访问 Cycript 未提供的内容(例如,在你正在探索的进程中),则你可以使用 C 语法中的
extern "C"
来声明函数的原型(这也可以是一个表达式)cy# extern "C" int getuid(); (extern "C" int getuid()) cy# getuid() 501
有时候,你会通过其他方式获取指向函数的指针,例如通过
dlopen
和dlsym
(它是由 Cycript 提供给你的,以及像RTLD_DEFAULT
这样的常量)。在调用这些函数之前,你需要将它们强制转换为正确的类型(可以选择间接的函数指针,或直接的 C 函数类型)cy# getuid = dlsym(RTLD_DEFAULT, "getuid") (typedef void*)(0x7fff885f95b0) cy# getuid() throw new Error("cannot call a pointer to non-function") cy# getuid = (typedef int())(getuid) (extern "C" int getuid()) cy# getuid() 501
-
可变函数(Variadic Functions)
可变函数是 C 语言中的一项特性,你可以将额外的参数传递给函数。这些额外的参数的类型是在调用时确定的,因此无法从函数中推断出来。因为 JavaScript 中没有不同的数字类型,所以权衡取舍之后选择了
int
,并且会在运行时检查参数所占内存的大小cy# printf("%c %hi %i %s\n", "7".charCodeAt(0), 7, 7, "7") 7 7 7 7 cy# NSLog(@"%@ %i\n", @"7", 7) 2016-01-09 21:17:29.895 cycript-apl[94629:507] 7 7 cy# printf("%i\n", 3.4) throw new Error("could not infer type of argument '3.4'")
这样做的部分原因是:在随机情况下使用的大多数可变参数,要么是指针(很容易推断),要么是
int
(这是一个合理的默认值),要么是小于int
的整数类型(例如unsigned short
)。对于可变函数的调用,C 语言将所有小于int
的类型提升为int
对于大于
int
的类型(例如long long
),或通常与int
不兼容的类型(例如double
),你需要对参数进行类型转换。请注意,在 Cycript 中类型转换小于int
的值使用的是 C 的类型提升规则,并返回一个简单的int
类型。这实际上是由于 Objective-C 的BOOL
而被迫做出的决定cy# printf("%lli %g\n", (typedef long long)(7), double(7)) 7 7 cy# typeid(short(7)) (typedef int)
-
C++11 的 Lambda 语法(C++11 Lambda Syntax)
有时,出于各种原因,你需要能够动态创建函数。你可以使用 C++11 的 Lambda 语法 来做到这一点(有些人一开始可能会觉得这种语法很奇怪)。请注意,出于各种原因,Cycript 仅支持指定
&
的捕获子句。生成的 Lambda 在 JavaScript 中的范围内关闭cy# qsort (extern "C" void qsort(void *, unsigned long, unsigned long, int (*)(void const*, void const*))) cy# var array = (typedef int[3])([6, 1, 3]) [6,1,3] cy# qsort(array, array.length, int.size, [&](const int *l, const int *r)-> int { return *l < *r ? -1 : *l > *r ? 1 : 0; }) cy# array [1,3,6]
C 结构体和数组(C Structs and Arrays)
有时候,我真不敢相信这真的有这么好的效果
-
结构体(Structures)
在遇到结构体之前,你不会在 C 语言中走得太远。在 C 语言中,结构体保存在与(函数和其他变量)不同的命名空间中。虽然在 C++ 中,有一个 fallback 允许访问结构体而不用声明
struct
,但是在 Cycript 中并不支持这一点(虽然它可以在全局级别上工作,但是函数作用域会使这变得复杂)定义结构体的语法与 C 语言类似:使用
struct
关键字、大括号、带名称的字段、等等。值得注意的一件事情是:将typedef
用于匿名结构体的 C 习惯用法,不仅适用于typedef
语句,而且也作为typedef
表达式的一部分,并且这就是结构体类型作为 CYON 输出的方式cy# struct Test { int x; int y; } (typedef struct { int x; int y; })
就像使用原生类型一样,你可以使用
new
运算符分配结构体的新实例,这将返回一个指针(我曾经为这个特殊的决定感到痛苦,半夜躺着几个小时盯着天花板,十分沮丧。我目前认为这是一个正确且一致的决定)在使用字段名称作为属性之前,字段访问需要一个间接指针。Cycript 提供了 C 中的箭头运算符
->
,使这更容易和更熟悉。当你访问并设置此结构体上的字段的值时,这些值将被存储在内存中,并与 C 中使用的结构体兼容cy# (struct CGRect) (typedef struct { struct CGPoint origin; struct CGSize size; }) cy# rect = new (struct CGRect) &{origin:{x:0,y:0},size:{width:0,height:0}} cy# rect->origin.x = 13 13 cy# rect->size.height = rect->origin.x (typedef double)(13) cy# rect &{origin:{x:13,y:0},size:{width:0,height:13}}
每当你想要设置一个结构体的值时(例如:使用
new
构造的初始化器,或对现有结构体字段的赋值),你可以使用与结构体具有相似结构的对象,而不是使用结构体类型的合法值。你还可以使用数组来初始化结构体(以及使用数组的索引访问结构体的字段)cy# rect = new (struct CGRect) &{origin:{x:0,y:0},size:{width:0,height:0}} cy# rect->origin = [6, 12] [6,12] cy# (*rect)[1] = {height: 78, width: 44} {height:78,width:44} cy# rect &{origin:{x:6,y:12},size:{width:44,height:78}}
你还可以使用 C 的地址操作符
&
获取结构体(或结构体的某一部分)的地址。你会得到一个Pointer
类型的对象。此特性目前不适用于基本类型的值(在编写这个手册时,我几乎修复了它,但是最后决定再坚持一段时间; 如果你需要它,则你请告诉我)从技术上讲,你可以对这些指针进行指针运算,但是我不特别推荐这样做:这是因为指针上的
.valueOf()
返回一个数字,这是内置数学运算符所必需的。问题是,这不是一种类型感知机制,所以所有的内容都是一个字节,但它对于演示目的却很有用cy# rect = new (struct CGRect) &{origin:{x:0,y:0},size:{width:0,height:0}} cy# &rect->origin &{x:0,y:0} cy# &rect->origin - rect 0 cy# &rect->size - rect 16 cy# &rect->size.width throw new TypeError("'undefined' is not a function (evaluating 'rect.$cyi.size.width.$cya()')")
-
数组(Arrays)
C/C++ 中的数组是内部不一致的数据类型。我已经用 C++ 编程了 20 年,但令我震惊的是,
new[]
是使用类型自省来实现的,而不是作为一种语法结构。如果你尝试编译以下代码,则你会收到一个错误,因为new int[]
返回int *
,即使我们声明了一个int(*)[]
#include <iostream> #include <typeinfo> template <typename Type_> void f() { Type_ *a = new Type_; } int main() { f<int[3]>(); return 0; }
欢迎你在 Cycript 中,在数组类型上,使用
new
运算符分配一个新数组,但它会做一些你不想要做的事情。我也还没有实现这个特性的语法版本(虽然我打算这样做;我在编写手册时才意识到这是一个严重的问题)。分配数组的合理方法是使用构造函数强制转换cy# (typedef int[3])([2, 4, 6]) [2,4,6] cy# var x = [6, 7, 8] [6,7,8] cy# x = (typedef double[x.length])(x) [6,7,8] cy# typeid(x[0]) (typedef double)
-
C 字符串(C Strings)
我觉得 Cycript 实现 C 字符串的方式真是太有趣了。要记住的关键点是,C++ 中有三种不同的字符类型:
signed char
、unsigned char
、char
;这与其他整数类型不同,整数类型只有带符号和无符号的变体。Cycript 将字符(character)模拟为一个字节长的字符串(string)(是的,一个字节长)cy# (typedef char[3])("dsa") ["d","s","a"] cy# p = (typedef char *)("dsa") &"dsa" cy# p[0] "d" cy# typeid(p[0]) (typedef char) cy# p[0] = "Q" "Q" cy# p &"Qsa"
从 JavaScript 字符串桥接的 C 字符串的内存会被垃圾回收(抱歉)。如果你从其他地方获取了内存,则你将不得不自己释放它。如果要将底层内存作为一堆数字而不是一堆字符串访问,则你可以将内存转换为指向数组的指针(是的,指向数组的指针)
cy# strdup (extern "C" char *strdup(char const*)) cy# p = (typedef const char *)("dsa") &"dsa" cy# p = strdup(p) &"dsa" cy# p = (typedef unsigned char (*)[p.length])(p) &[100,115,97] cy# p = (typedef struct { char a; char b; char c; } *)(p) &{a:"d",b:"s",c:"a"} cy# free(p)
-
Placement New
在 C++ 中,你可以使用
new
运算符分配对象,但将现有内存块的地址传递给它,以在该内存块中构造新类型的值。这是能够输入强制转换指针并执行赋值的精神伴侣。我很确定这对 Cycript 来说是件好事cy# p = malloc(12) (typedef void*)(0x100936110) cy# (typedef signed char (*)[12])(p) &[0,0,0,0,0,0,0,-48,24,56,9,16] cy# p = malloc(12) (typedef void*)(0x1008c8470) cy# memset(p, 0, 12) (typedef void*)(0x1008c8470) cy# (typedef signed char (*)[12])(p) &[0,0,0,0,0,0,0,0,0,0,0,0] cy# double.call(p, 31337) 31337 cy# (typedef signed char (*)[12])(p) &[0,0,0,0,64,-102,-34,64,0,0,0,0] cy# (typedef double (*)[1])(p) &[31337] cy# free(p)
Objective-C 基础(Objective-C Basics)
大多数时候,你实际上是在处理 Objective-C 对象
-
数据结构(Data Structures)
Objective-C 的语法通常涉及大量的
@
符号。最常见的是,你会在字符串常量之前看到@
符号来标识NSString
的实例,而不是原始的 C 字符串。在最近的 Objective-C 版本中,Apple 为 Objective-C 的数组和字典添加了特殊语法。Cycript 支持所有的这些语法// 定义 NSString 类型的对象 cy# @"hello" @"hello" // 定义 NSArray 类型的对象 cy# @[2, 4, 5] @[2,4,5] // 定义 NSDictionary 类型的对象 cy# @{"thing": 9, "field": 10} @{"thing":9,"field":10} // 将布尔值 YES 包装成 NSNumber 类型的对象 cy# @YES @true // 将空值 null 包装成 NSValue 类型的对象 cy# @null @null // 计算 5 + 7 的结果,并将其包装成 NSNumber 类型的对象 cy# @(5 + 7) @12
这些 Objective-C 数据类型中的每一个,都使用 JavaScript 中适当的本地类型桥接到了 JavaScript。这是通过将 JavaScript 类放在 Objective-C 对象的原型继承链中来完成的。我们可以使用 JavaScript 的
instanceof
操作符看到这一点(@null
没有 JavaScript 的模拟)cy# @"hello" instanceof String true cy# @[2, 4, 5] instanceof Array true cy# @{"thing": 9, "field": 10} instanceof Object true cy# @YES instanceof Boolean true cy# @5 instanceof Number true
因为这种桥接,所以通常可以在 Objective-C 类型上使用 JavaScript 方法。特别是,熟悉 JavaScript 的开发人员会发现使用数组和字符串时非常的容易,因为所有适当的特定于 JavaScript 的方法都应该有效。请注意,它们的返回值将是 JavaScript 原生的
cy# @"hello".substr(1, 3) "ell" cy# @[2, 4, 5, "hello"].slice(1, 3) [@4,@5]
Cycript 还桥接了属性的
get
语法(通过方括号[]
或点号.
,因为它们在 JavaScript 中是等价的)以用于NSDictionary
和NSArray
。undefined
的 JavaScript 值也以某种方式桥接,使 JavaScript 的开发人员对NSArray
的值感到熟悉(或者只是特别害怕)cy# var a = [NSMutableArray arrayWithObjects:[2, 4, 6] count:3] cy# a.length 3 cy# a[10] = 6; a @[2,4,5,,,,,,,,6] cy# a.length = 4; a @[2,4,5,,] cy# a[3] undefined cy# a[0] @2 cy# @{"field": 5}["field"] @5
在底层,Objective-C 对象由 Cycript 使用
Instance
类型的对象管理。@
语法是通过调用Instance.box
来实现的,Instance.box
采用 JavaScript 数据结构,并将其转换为 Objective-C 的原生数据结构。老实说,我鼓励你在使用 Cycript 时不要考虑这一点,但它有助于理解后面的示例cy# @(2 + 7) cy= Instance.box((9)) @9 cy# @"hello" cy= Instance.box("hello") @"hello" cy# @[2, 4, 5] cy= Instance.box([2,4,5]) @[2,4,5]
-
方法选择器和消息(Selectors and Messages)
Objective-C 不是基于调用方法的思想,而是基于发送消息的思想。这些消息的语法涉及到一组方括号内的参数的关键字中缀表示法。消息的名称被称为方法选择器(
Selector
),并且 Objective-C 的编译器会将这些消息的名称转换为对objc_msgSend
函数的调用。在 Cycript 中也是如此cy# ?debug debug == true cy# [@"hello" stringByReplacingOccurrencesOfString:@"ell" withString:@"ipp"] cy= objc_msgSend(Instance.box("hello"),"stringByReplacingOccurrencesOfString:withString:",Instance.box("ell"),Instance.box("ipp")) @"hippo"
按照规定,
objc_msgSend
函数的第二个参数是一个方法选择器(Selector
),方法选择器(Selector
)实际上是一个 C 字符串(因为它是一个内部字符串,所以它只有一个副本,并且它的值可以通过指针进行比较)。在这种情况下,Cycript 会自动从字符串中构建一个方法选择器(Selector
),但我们也可以使用@selector
来获取一个实际的方法选择器(Selector
)对象cy# ?debug debug == true cy# @selector(this:is:a:message:) cy= sel_registerName("this:is:a:message:") @selector(this:is:a:message:)
以一种奇怪的方式,Objective-C 中的方法选择器(
Selector
)相当于 C++ 中的(指向成员函数的指针)类型。由于这个类比,使 Cycript 的Selector
类型派生自 JavaScript 的Function
是合理的。这意味着你可以在方法选择器(Selector
)上使用.call
向对象发送任意消息(这通常比 Cycript 的objc_msgSend
感觉更好)cy# var capitalize = @selector(capitalizedString) cy# capitalize.call(@"hello") @"Hello"
显然,Objective-C 的方法选择器(
Selector
)可能会非常冗长:JavaScript 使用一个单词replace
,而 Objective-C 使用完整的短语string By Replacing Occurrences Of String ... with String ...
。值得庆幸的是,Cycript 的制表符代码补全非常适合这些情况:它在运行时计算对象,并从可能的方法选择器(Selector
)中进行补全如果你将 JavaScript 的值传递给 Objective-C 的消息,它将被桥接到一些合理的 Objective-C 类型。请注意,此桥将在可能的情况下保留原始对象的身份,并且也适用于消息的目标,包括它们的参数。这意味着我们可以使用 Objective-C 来操作 JavaScript 的数据
cy# var a = [2, 4, 6] [2,4,6] cy# [a objectAtIndex:0] @2 cy# [a setObject:"hello" atIndex:2]; a [2,4,@"hello"] cy# var o = {field: 4} {field:4} cy# [o setObject:a forKey:"value"]; o {field:4,value:[2,4,@"hello"]}
-
类与内存分配(Classes and Allocation)
Objective-C 中的类可用作 Cycript 中的变量。Objective-C 中的类本身是 Objective-C 中对象的一种形式(称之为类对象):你通常需要使用类消息来获取类对象的值(例如,
[NSObject class]
),这一事实更多的是因为 C 语法的限制,而不是运行时环境语义的一部分。Cycript 解决了这个问题cy# NSObject NSObject cy# [NSObject class] NSObject cy# objc_getClass("NSObject") NSObject
为了分配指定对象的一个实例,你可以使用普通的 Objective-C 范式发送
alloc
消息。Objective-C 的内存管理基于引用计数(reference counting),并且 Cycript 将保留 JavaScript VM 对对象的引用。然而,alloc
也是返回一个引用计数增加的对象:你应该自动释放它cy# [[[NSObject alloc] init] autorelease] #"<NSObject: 0x10050e220>"
也就是说,Cycript 提供了一种更加面向 JavaScript 的语法来为你处理
alloc/autorelease
模式:你可以在类类型上使用new
运算符,此时你只需发送初始化消息(init
)。由于 JavaScript 和 Objective-C 的语法优先级的计算方式,你只需要一组方括号或圆括号cy# [new NSObject init] #"<NSObject: 0x100409930>"
我还必须指出,JavaScript 本身就是使用垃圾回收机制的。一旦对象被 JavaScript 触及,这可能会对对象的生命周期造成一些混淆:这最终会导致对象的生命周期持续非常长(因为 JavaScript 没有真正的理由经常进行垃圾回收)。你可以使用
?gc
或调用gc()
强制进行垃圾回收cy# ?gc collecting... done. cy# gc() cy#
请注意,显式的垃圾回收并不适用于 JavaScriptCore 的所有底层版本:2012 年 4 月,WebKit 决定
JSGarbageCollect
API 不应实际回收垃圾(Bug 84476),直到一年后才添加了新的 API 以提供此关键功能(Bug 111088)。我真诚地希望 Apple 不要改变主意并再次删除它 -
对象描述和字段访问(Description and Field Access)
在上一节中,我们看到对象以带有
#
前缀的字符串形式输出。这是因为 Cycript 向你显示了对象的描述(description),Objective-C 中的所有对象都应该提供对象的描述(description)。但是,为了将这些对象描述与实际字符串区分开来,使用#
而不是@
。#
代表 Cycript 中的一个对象如果你已经有了一个指向对象的指针(可能是你从 LLDB 调试器获得的指针,或者你在上一个命令输出的对象描述中看到的指针),你可以使用
#
将对象具化回 JavaScript。当然,如果该对象不存在或者已经被释放,则这很可能会导致 Cycript 崩溃cy# UIApp #"<SpringBoard: 0x10e803490>" cy# s = #0x10e803490 #"<SpringBoard: 0x10e803490>" cy# #0 nil cy# #4 Segmentation fault: 11
通常,通过对象的描述足以理解该对象的作用。但是,有时你需要更多,例如:你真正感兴趣的是由对象的字段表示的对象的内容。Cycript 允许你使用
->
访问对象的字段。它通过从*
运算符返回对象的结构表示来做到这一点cy# c = [[UIApp windows][0] contentView] #"<UIView: 0x10e883d40; frame = (0 0; 320 568); layer = <CALayer: 0x10e883e00>>" cy# c->_subviewCache @[#"<SBFStaticWallpaperView: 0x11459fc40; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x11459ee70>>"] cy# *c {isa:"UIView",_layer:#"<CALayer: 0x10e883e00>",_gestureInfo:null,_gestureRecognizers:null,_subviewCache:@[#"<SBFStaticWallpaperView: 0x11459fc40; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x11459ee70>>"],_charge:0,_tag:0,_viewDelegate:null,_backgroundColorSystemColorName:null,_countOfMotionEffectsInSubtree:1,_viewFlags:@error,_retainCount:8,_tintAdjustmentDimmingCount:0,_shouldArchiveUIAppearanceTags:false,_interactionTintColor:null,_layoutEngine:null,_boundsWidthVariable:null,_boundsHeightVariable:null,_minXVariable:null,_minYVariable:null,_internalConstraints:null,_constraintsExceptingSubviewAutoresizingConstraints:null}
// 上面的代码经过整理后,如下所示: cy# c = [[UIApp windows][0] contentView] #"<UIView: 0x10e883d40; frame = (0 0; 320 568); layer = <CALayer: 0x10e883e00>>" cy# c->_subviewCache @[#"<SBFStaticWallpaperView: 0x11459fc40; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x11459ee70>>"] cy# *c { isa:"UIView", _layer:#"<CALayer: 0x10e883e00>", _gestureInfo:null, _gestureRecognizers:null, _subviewCache:@[ #"<SBFStaticWallpaperView: 0x11459fc40; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x11459ee70>>"], _charge:0, _tag:0, _viewDelegate:null, _backgroundColorSystemColorName:null, _countOfMotionEffectsInSubtree:1, _viewFlags:@error, _retainCount:8, _tintAdjustmentDimmingCount:0, _shouldArchiveUIAppearanceTags:false, _interactionTintColor:null, _layoutEngine:null, _boundsWidthVariable:null, _boundsHeightVariable:null, _minXVariable:null, _minYVariable:null, _internalConstraints:null, _constraintsExceptingSubviewAutoresizingConstraints:null }
-
异常(Exceptions)
正如人们在这一点上所期望的那样,Objective-C 的异常与 JavaScript 是来回桥接的。你可以在 JavaScript 中捕获由 Objective-C 抛出的异常,或者让它一直抛出到控制台,在那里它将以某种希望有用的方式呈现。Cycript 自己的 C++ 异常也被进行了桥接
cy# var a; try { [[NSMutableArray array] setObject:nil atIndex:0]; } catch (e) { a = e; throw e; } throw #"*** -[__NSArrayM setObject:atIndex:]: object cannot be nil" cy# a #"*** -[__NSArrayM setObject:atIndex:]: object cannot be nil"
在某些情况下,你会发现自己的代码正常运行,但控制台本身在打印对象时却捕获到了异常。在这些情况下,你将看到
@error
被打印到输出中。这种情况通常发生在 Cycript 不太支持的数据类型上,例如:位域(Bit Field)(如上所述的_viewFlags
)
Objective-C 高级(Objective-C Advanced)
本章节中的一些东西有可能在范围内受到限制,或有可能会发生变化
-
对 @property 的支持(@property Support)
除了字段(Objective-C 实际上称之为
instance variable
或ivar
)之外,Objective-C 还有一个属性(property
)的概念。Objective-C 编译器本质上将属性(property
)实现为语法转换:使用点语法.
访问对象指针的属性,会使用getter/setter
命名约定转换为对应的 Objective-C 消息然而,开发人员也可以使用
@property
语法在他们自己的类上定义显式属性,这是在运行时可用的信息。这允许人们区分名词(属性语法很合理,现在在带有getter/setter
的 ECMAScript 5 中得到支持)和动词(属性语法几乎是危险的)Cycript 用于实现 Objective-C 编译器的行为。造成这种情况的部分原因是:虽然 Apple 自己的框架通常在其公共头文件中具有
@property
的定义,但是 Apple 显然实际上并未在框架内部使用属性语法,因此他们编译的二进制文件经常缺少在运行时使用的所有属性的信息然而,Apple 最近一直在推动开发人员使用 Swift,这是一种新的编程语言,他们似乎也没有在(框架的)内部使用。对于 Swift,Apple 真的很想解决这个名词/动词的问题,因此不得不返回并修复他们所有的旧库以在内部拥有必要的
@property
定义。Swift 现在要求你调用动词我的 Cycript 待办事项之一是与 Swift 更好地集成,并且我从不喜欢隐式属性,因此 Cycript 不再支持它们。也就是说,为了与使用 WebCycript 的产品中提供的现有代码保持一致性(例如,Cydia 中大量的 Cydget),我已将此功能保留在了 Cydget 的上下文中
我还竭尽全力使访问这些属性的肌肉记忆合理兼容,事实证明,这也适用于 Swift 命令行环境:如果你尝试使用制表符补全不带参数的动词,则你可以一直补全到
()
(总有一天,我会使(
的代码补全功能适用于 JavaScript 函数)也就是说,我很纠结,并且对人们支持和反对无处不在的隐式属性的争论感兴趣。同时,我还开启了运行时的隐式属性,其中
@property
定义非常少,以至于即使像在NSObject
上使用.description
这样的基本操作也不起作用(但请记住:自动动词auto-verb
在 JavaScript 中令人困惑)UIApplication.sharedApplication().windows[0].contentView().subviews()[0] #"<SBFStaticWallpaperView: 0x1590ca730; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x1590cabd0>>"
-
原型和消息(Prototypes and Messages)
在 JavaScript 中,所有的对象都有一个
constructor
属性,该属性引用了用于创建对象的构造函数。然后,此构造函数具有一个prototype
属性,可用于扩展该类的所有实例的功能。Cycript 为 Objective-C 对象正确地模拟了此种行为cy# @6..constructor __NSCFNumber cy# __NSCFNumber.superclass() NSNumber cy# NSNumber.prototype.add5 = function() { return this + 5; } cy# @6..add5() 11
因为 Objective-C 的类系统实际上来自与 JavaScript 相同的继承体系(通过 self 的 Smalltalk 语法),所以将 Objective-C 消息表公开并集成到 JavaScript 原型链中是合理的。如果你需要替换现有消息或从另一个对象复制消息,则 JavaScript 会暴露给 Objective-C
cy# var oldm = NSObject.prototype.description (extern "C" id ":description"(id, SEL)) cy# NSObject.prototype.description = function() { return oldm.call(this) + ' (of doom)'; } cy# [new NSObject init] #"<NSObject: 0x100d11520> (of doom)"
这个功能以前是使用一个单独的命名空间公开的:你必须使用
messages
,而不是prototype
。现在已经不是这样了 -
类别和类(Categories and Classes)
在 Objective-C 中,你可以拥有一个类别(Category),它是一组针对指定类的(替换或添加的)消息实现。Cycript 中支持此语法。对于替换的方法,你可以省略方法的类型签名,因为 Cycript 可以从现有类的对应方法中推断出方法的类型签名。对于添加的方法,你需要指定方法的返回值与参数的确切类型,以提供方法的类型签名
cy# @implementation NSObject (MyCategory) -description { return "hello"; } -(double) f:(int)v { return v * 0.5; } @end cy# o = [new NSObject init] #"hello" cy# [o f:3] 1.5
完全成熟的 Objective-C 类可以使用
@implementation
声明,@implementation
也替换了 Objective-C 的@interface
(这实际上在 Objective-C 中是有效的)。你甚至可以使用 C 类型语法声明成员变量(因为这些类型非常重要)。如果你想查看此语法如何对 JavaScript 进行脱糖(desugar),则它会调用记录在案的 Objective-C 运行时函数cy# ?debug debug == true cy# @implementation A : NSObject { int field; } -(int) message { return this->field; } @end cy= (function(s,r,i,n,t,e){r=object_getClass(s);i=objc_allocateClassPair(s,"A",0);e=object_getClass(i);{t=int.toString();n=new Type(t);class_addIvar(i,"field",n.size,n.alignment,t)}{n=sel_registerName("message");t=int.toString()+"@:";class_addMethod(i,n,new Functor(function(n,t){var e;e=new objc_super(n,s);return function(){var e;e=this;return e.$cyi.field}.call(n)},t),t)}objc_registerClassPair(i);return i})(NSObject) A cy# ?debug debug == false cy# a = [new A init] #"<A: 0x10080c540>" cy# a->field = 10 10 cy# [a message] 10
// 上面的代码经过整理后,如下所示: cy# ?debug debug == true cy# @implementation A : NSObject { int field; } -(int) message { return this->field; } @end cy = (function(s, r, i, n, t, e) { r = object_getClass(s); i = objc_allocateClassPair(s, "A", 0); e = object_getClass(i); { t = int.toString(); n = new Type(t); class_addIvar(i, "field", n.size, n.alignment, t) } { n = sel_registerName("message"); t = int.toString() + "@:"; class_addMethod(i, n, new Functor(function(n, t) { var e; e = new objc_super(n, s); return function() { var e; e = this; return e.$cyi.field }.call(n) }, t), t) } objc_registerClassPair(i); return i })(NSObject)A A cy# ?debug debug == false cy# a = [new A init] #"<A: 0x10080c540>" cy# a->field = 10 10 cy# [a message] 10
-
代码块(Blocks)
与原始函数指针不同,Objective-C 的 Lambda 表达式作为一种特殊形式的对象被传递,称为
block
。Cycript 支持创建和调用block
,并具有类似于 Objective-C 的特殊语法,以让开发者可以尽快上手。与 Objective-C 不同的是,Cycript 中block
的返回值类型不是可选的,而是必须指定的cy# block = ^ int (int value) { return value + 5; } ^int(int){} cy# block(10) 15
Java 初始预览版(Java Initial Preview)
Cycript 现在支持 Java,并可以在 Android 上运行
-
基本语法(Basic Syntax)
在 Java 中,类在语法上与表示类的对象不兼容,因而 Cycript 保持了这种区别。类被组织到称为
Package
的命名空间中,可以使用顶级Package
变量访问(类似于 Rhino;虽然目前尚不支持 Nashorn 的 Java.type,但是很快就会支持)。还提供了通用前缀可以使用
new
运算符来创建指定类的实例,可以使用点语法.
进行方法的调用。在使用 Java 时,必须考虑的主要问题是:函数可以通过其参数的类型进行重载(overload)。Cycript 构建重载解析集,并将尝试查找兼容的函数。使用类型强转可以帮助指导实现cy# Packages.java.util.HashMap java.util.HashMap cy# hash = new java.util.HashMap #"{}" cy# hash.put("hello", 7) null cy# hash.put("world", int(7)) null cy# hash #"{world=7, hello=7.0}"
Java 有一个单独的命名空间,用于从方法中访问字段。为了在 Cycript 中正确地模拟此特性,箭头运算符
->
是从 Objective-C 借用的。为了快速地了解对象的字段的值,可以使用 C 中的指针间接运算符(*
)。否则,Java 对象通常使用带有#
的toString
的结果来呈现cy# hash = new java.util.HashMap #"{}" cy# *hash {entrySet:#"[]",keySet:null,loadFactor:0.75,modCount:0,size:0,table:null,threshold:0,values:null} cy# hash->loadFactor 0.75
-
动态代理(Dynamic Proxies)
Java 中任何重要的远程工作都需要有实现接口的能力。Java 1.3 中支持被称为
dynamic proxies
的功能,它允许(调用处理程序Handler
)代理(另一个对象的接口)。此特性在 Cycript 中被作为接口上的构造函数参数公开。这是一个使用 Jetty 设置网络服务器的示例cy# var server = new org.eclipse.jetty.server.Server; 2016-01-09 22:34:20.785:INFO::main: Logging initialized @546ms #"org.eclipse.jetty.server.Server@b81eda8" cy# var connector = new org.eclipse.jetty.server.ServerConnector(server); #"ServerConnector@50134894{HTTP/1.1,[http/1.1]}{0.0.0.0:0}" cy# connector.setPort(8080); cy# server.addConnector(connector); cy# server.setHandler(new org.eclipse.jetty.server.Handler({ isRunning() { return false; }, // XXX: o_O handle(target, base, request, response) { response.setStatus(200); response.setContentType("text/plain; charset=utf-8"); var writer = response.getWriter(); writer.print([new NSObject init].toString()); base.setHandled(true); }, })); cy# connector.open(); cy# server.start(); 2016-01-09 22:35:04.137:INFO:oejs.Server:main: jetty-9.3.6.v20151106 2016-01-09 22:35:04.179:INFO:oejs.ServerConnector:main: Started ServerConnector@50134894{HTTP/1.1,[http/1.1]}{0.0.0.0:8080} 2016-01-09 22:35:04.179:INFO:oejs.Server:main: Started @43971ms
$ curl http://localhost:8080/; echo <NSObject: 0x100e22620>
这个例子实际上有点令人沮丧,因为 Jetty 期望你继承
Handler
接口的抽象实现,而不是从头开始实现该接口。目前,虽然 Cycript 不支持动态子类化(这需要生成运行时类文件),但是可以在未来版本中使用不同的技术来实现 -
安卓支持(Android Support)
借助对 Java 的支持,Cycript 现在可以在 Android 上使用。虽然 Cycript 一直支持桌面 Linux,但是还需要做更多的工作才能使 Cycript 在 Android 上良好运行。从控制台运行 Cycript,设置一个可以访问所有 Android 运行时库的完整环境。然而,尚不支持进程注入(需要更新
cynject
)请注意,我在这个初始版本中发布的
libJavaScriptCore.so
的构建来自 Ejecta -X,因为在发布这个新版本的 Cycript 之前,我还没有弄清楚如何为 Android 编译JavaScriptCore
,因为我已经比我想象的多花了一个月的时间。我很快就会着手构建更好的JavaScriptCore
shell@flounder:/data/local/tmp $ ./cycript WARNING: linker: libJavaScriptCore.so has text relocations. This is wasting memory and prevents security hardening. Please fix. cy# android.os.Looper.prepare() cy# var activity = new android.app.Activity #"android.app.Activity@13538a68"
请注意,Java 的桥接目前还有其他限制。我非常兴奋能够让它更好地工作,并希望结果与用于 Objective-C 的 Cycript 一样令人惊叹,但我也认为它已经足够有用,可以开始获取反馈了,因此我决定现在就发布它。来 IRC 告诉我你的经历吧!
对象查询(Object Queries)
如何通过过滤所有对象的集合,来找到指定对象的实例
-
选择真理(The Axiom of Choice)
当人们第一次拿起 Cycript 时,问的一个更常见的问题是:我如何找到指定类的一个实例,以便向它发送消息?这个问题似乎是合理的:你想了解某类的对象是如何工作的,为此你需要该类对象的一个实例来进行调试。你知道在内存里有很多这样的对象,而你只想要其中一个
然而,对象并不是这样工作的:在内存中的任何地方都没有保存特定类的所有实例的列表。要做到这一点,你必须在创建该类的任何实例之前,hook 该类的
alloc
和init
例程,然后(在为稍后你可以查询到的某些数据结构创建该类的实例时)跟踪它们正因为如此,我的回应总是:那是不可能的!这是真的:在一般情况下没有办法可以解决这个问题,因为大多数语言的运行时中根本不存在该信息(指的是:保存特定类的所有实例的列表),包括 Objective-C。但是,事实证明,可以非常接近地完成这项工作以进行调试
-
choose(Class)
可以解决此问题的方法是:使用已添加到 Cycript 中的名为
choose
的函数。此函数接收一个 Objective-C 类作为参数,并尝试扫描堆空间,以查找与你指定的类(或该类的任何子类)大小和形状相匹配的内存区域(hcg 注: 简单地说,就是在内存的堆空间中查找指定的类及其子类的所有实例对象)cy# choose(SBIconModel) [#"<SBIconModel: 0x1590c8430>"]
虽然有时它会崩溃,但是通常它会起作用。因为可能会找到多个实例,所以扫描的结果是一个数组。在某些情况下,这个数组可能非常大。例如,如果尝试在 SpringBoard 中查找图标的视图,则可能会返回数百个对象。幸运的是,JavaScript 是一种编程语言
cy# var views = choose(SBIconView) [#"<SBIconView: 0x159460fa0; frame = (27 92; 60 74); opaque = NO; gestureRecognizers = <NSArray: 0x159518ae0>; layer = <CALayer: 0x159461220>>",#"<SBIconView: 0x159468e50; frame = (114 356; 60 74); opaque = NO; gestureRecognizers = <NSArray: 0x15946d2f0>; layer = <CALayer: 0x1592c9a70>>",... cy# for (var view of views) if ([[[view icon] application] displayName] == "Photos") photos = view; photos; #"<SBIconView: 0x15fc75e90; frame = (201 4; 60 74); opaque = NO; gestureRecognizers = <NSArray: 0x15fbfacc0>; layer = <CALayer: 0x15fc76110>>"
如果需要返回多个结果,则可以创建一个数组,然后
push
所有匹配的结果。但是,当每次你想要添加过滤器时,这样做会非常烦人。幸运的是,JavaScript 1.7 中有一个被称为数组解析(array comprehension)的特性,可以像使用微型查询语言(miniature query language)一样使用它来过滤对象数组此外,Cycript 还采用了
?.
语法(在包括 Swift 在内的各种语言中都可以看到)在 JavaScript 级别提供功能,类似于 Objective-C 允许将任何消息(具有兼容的返回值类型)发送到nil
对象,其结果将为nil
。在null/undefined
上使用?.
调用方法和访问成员将返回null/undefined
cy# [for (view of views) if (view.icon?.application()?.displayName() == "Photos") view] [#"<SBIconView: 0x15fc75e90; frame = (201 4; 60 74); opaque = NO; gestureRecognizers = <NSArray: 0x15fbfacc0>; layer = <CALayer: 0x15fc76110>>"]
进程注入(Process Injection)
Cycript 的大多数用法实际上是在其他进程内部工作的
-
# cycript -p
虽然在本地控制台中玩 Cycript 对于 Objective-C 本身,甚至是对于特定库的使用来说可能是一个很好的学习体验,但是 Cycript 的大多数实际使用涉及了解如何构建现有的应用程序,而这通常最好是通过从应用程序内部操作进程来完成(甚至是只可能通过从应用程序内部操作进程来完成)
要注入另一个进程,你可以使用 Cycript 的
-p
标志。你可以传递你想要定位的另一个进程的进程 ID,也可以传递正在运行的进程的名称。如果你传递了一个进程的名称,则 Cycript 将运行ps
命令并尝试查找该进程的标识符(process identifier)。虽然 Cycript 有时可能会查找失败,但是几乎所有的时间 Cycript 都会查找成功然后,你将看到 Cycript 控制台的一个实例,控制台中的所有命令都将被发送到远程进程,并在有权访问 Cycript 桥接支持的进程内部运行的 JavaScript 实例中执行,最后将任何执行结果发送回你的本地控制台。此 VM 实例在多个 Cycript 控制台之间共享
iPhone:~# cycript -p SpringBoard cy# UIApp #"<SpringBoard: 0x10ea05f60>"
-
限制(Limitations)
Cycript 通过远程线程注入,将其库扔到另一个进程中来执行此注入。但这并不总是可能的,特别是,你的
user
用户需要修改其他进程的权限。这通常只需要你成为root
用户。如果你尝试使用 Cycript 注入你无法访问的进程,则在调用task_for_pid
函数时将报错iPhone:~$ cycript -p SpringBoard *** _krncall(task_for_pid(self, pid, &task)):../Mach/Inject.cpp(65):InjectLibrary [return=0x5] iPhone:~$ sudo cycript -p SpringBoard cy#
就算 Cycript 成功注入到了另一个进程,也不能保证 Cycript 会成功加载其库。由于沙盒的原因,这在最新版本的 macOS 上是相当常见的现象。在这些情况下,Cycript 将尝试为你提供错误信息,并且你可以查阅
/var/log/system.log
以获取更多信息MacBook:~$ sudo ./cycript -p Calculator dlopen(/Users/saurik/Cycript.lib/libcycript-any.dylib, 5): no suitable image found. Did find: /Users/saurik/Cycript.lib/libcycript-any.dylib: open() failed with errno=1 MacBook:~$ tail -n 100 /var/log/system.log | grep Calculator Jan 21 09:12:34 Jays-MacBook-Air.local sandboxd[22293] ([22284]): Calculator(22284) deny file-read-data /Users/saurik/cycript/Cycript.lib/libcycript-any.dylib
要处理打开库的沙盒问题,通常只需将 Cycript 安装到
/usr
就足够了。为此,请将 Cycript 的 dylib 文件放入/usr/lib
,并将 Cycript 的Cycript.lib/cycript
二进制文件作为/usr/bin/cycript
。你可能还想将 Cycript 模块目录(cycript0.9
)复制到/usr/lib
。然而,即使来自/usr
,Cycript 也并不总是有效MacBook:~$ sudo cp -a Cycript.lib/*.dylib /usr/lib MacBook:~$ sudo cp -a Cycript.lib/cycript-apl /usr/bin/cycript MacBook:~$ sudo cycript -p Calculator ^C MacBook:~$ tail -n 100 /var/log/system.log | grep Calculator Jan 21 09:14:40 Jays-MacBook-Air.local sandboxd[22293] ([22284]): Calculator(22284) deny network-outbound /private/tmp/.s.cy.27221
在这种情况下,不允许应用程序 Calculator 使用 Cycript 控制台分配的 Unix 域套接字进行连接。虽然可能还有其他方法可以解决这个问题,但是我还没有机会研究这么多。不管怎样,你应该不会在注入到 iOS 或 macOS 的 iOS 模拟器时,遇到任何问题
iOS 静态库(iOS Static Library)
-
你可以将 Cycript 嵌入到你的应用程序中,以便在未越狱的设备上进行调试
我在
360|iDev
上谈到过这个。开发人员可以观看该演讲的结尾,以了解如何做到这一点(虽然我打算在此处添加文档,但是我现在正在讨论这个)
Cydia Substrate
Substrate 是一种比 Swizzling 更合理的修改运行时行为的方法
正如本手册开头所述,Substrate 是我提供的一个单独的框架。虽然在许多方面,Substrate 与 Cycript 无关,但是用户倾向于在 Substrate 与 Cycript 之间频繁地来回切换。在 Cycript 的最新版本中,我提供了一个模块,可以帮助你使用来自 Cycript 的 Substrate 的更多功能
学习使用 Substrate 本身是一种完全独立的体验,并且 Substrate 有自己的网站,里面有很多的文档。我将在这里记录的,只是我通过 Cyript 模块公开的一些 Substrate(将来,这种支持可能会由 Substrate 记录,而不是 Cycript;甚至 Cycript 模块可能会随 Substrate 一起提供)
-
MS.hookMessage
当 Swizzling 消息的实现时,如果你弄乱了那些还没有拥有该消息的对象(并且仅从 superclass 继承它),则必须处理一些有趣的极端情况。Substrate 解决了这些问题,并且确保类不会被初始化。这会调用 MSHookMessageEx 函数
cy# @import com.saurik.substrate.MS cy# var oldm = {}; cy# MS.hookMessage(NSObject, @selector(description), function() { return oldm->call(this) + " (of doom)"; }, oldm) cy# [new NSObject init] #"<NSObject: 0x100203d10> (of doom)"
-
MS.hookFunction
Substrate 还允许你修改 C 函数的行为。因为 Substrate 已经可以从被 hook 的函数本身获取大部分可用的类型签名信息,所以你只需要传递替换函数即可。你可以将原始值捕获为函数指针的模拟。这会调用 MSHookFunction 函数
cy# @import com.saurik.substrate.MS cy# extern "C" void* fopen(char *, char *); cy# var oldf = {} cy# var log = [] cy# MS.hookFunction(fopen, function(path, mode) { var file = (*oldf)(path, mode); log.push([path.toString(), mode.toString(), file]); return file; }, oldf) cy# fopen("/etc/passwd", "r"); (typedef void*)(0x7fff774ff2a0) cy# log [["/etc/passwd","r",(typedef void*)(0x7fff774ff2a0)]]
接下来是什么?(What Next ?)
那些真正成功阅读了整本手册的用户,现在可能会感到焦虑
-
推荐(Recommendation)
从 Apple 下载 iOS SDK 并开始尝试进行酷炫的修改。如果你需要帮助来了解 Apple 的代码是如何组合在一起的,则我建议你观看今年 WWJC(World Wide JailbreakCon)上的一些演讲,从 Adam Bell 使用 Cycript 的演讲开始。你可能会想出一些很棒的东西!