Logos 语法简介
-
Logos 语法简介
Logos 是 Theos 开发套件中的一个组件,它允许使用一组特殊的预处理器指令,轻松且清晰地编写 hook 代码
Logos 提供的语法极大地简化了 MobileSubstrate 插件(即 Tweak)的开发。MobileSubstrate 插件可以在整个操作系统中 hook 其他方法。在这种情况下,hook 是指一种用于替换或修改在操作系统上的其他应用程序中发现的类的方法的技术
Logos 与 Theos 一起分发,开发者可以在任何由 Theos 构建的项目中使用 Logos 的语法,而无需进行任何额外的设置
Logos 语法由一系列预编译指令组成,按照功能分为以下 3 类:
- Block Level
- Top Level
- Function Level
-
相关链接
Block Level
Block 级别的指令将打开一个代码块,它必须由 %end
指令关闭。Block 级别的指令不应该存在于函数或者方法中
-
%group
指令格式:
// 分组用于条件初始化或者代码组织,同时,分组对于管理旧代码的向后兼容性也很有用 // 1.使用分组名 Groupname 开始一个 hook 分组 // 2.分组之间不能相互嵌套,即一个分组不能在另一个分组内 // 3.所有未分组的 hook 代码都位于隐式的 _ungrouped 分组中(所有不属于某个自定义分组的 hook 代码,都会被隐式地归类到 _ungrouped 分组中) // 4.如果没有其他分组,则默认会初始化 _ungrouped 分组,开发者也可以使用 %init 指令来手动初始化 _ungrouped 分组 // 5.其他分组必须使用 %init(Groupname) 指令进行初始化 %group Groupname
使用示例:
%group iOS8 %hook IOS8_SPECIFIC_CLASS // your code here %end // end hook IOS8_SPECIFIC_CLASS %end // end group iOS8 %group iOS9 %hook IOS9_SPECIFIC_CLASS // your code here %end // end hook IOS9_SPECIFIC_CLASS %end // end group iOS9 %ctor { if (kCFCoreFoundationVersionNumber > 1200) { %init(iOS9); } else { %init(iOS8); } }
-
%hook
指令格式:
// 为名为 Classname 的类打开一个 hook 代码块 // %hook 可以位于 %group 内 %hook Classname
使用示例:
%hook SBApplicationController -(void)uninstallApplication:(SBApplication *)application { NSLog(@"Hey, we're hooking uninstallApplication:!"); %orig; // 调用此方法的原始实现 return; } %end // end hook SBApplicationController
-
%new
指令格式:
// 通过在方法定义的上方添加 %new 指令,可以向被 %hook 指令 hook 的类或者 %subclass 指令动态生成的子类,添加一个新方法 // 参数 signature 是新添加的 Objective-C 方法的类型编码,如果省略参数 signature,则 Logos 会自动生成一个 // %new 必须位于 %hook 或者 %subclass 内 %new %new(signature)
使用示例:
// 因为 Logos 不会自动声明新添加的方法,所以开发者应该在 @interface @end 中手动声明新添加的方法 @interface TheClass (TweakMethods) -(void)yourNewMethod; @end %hook TheClass %new -(void)yourNewMethod { /* code */ } %end // end hook TheClass
-
%subclass
指令格式:
// %subclass 指令用于在运行时为给定的类 Classname 动态地创建子类,并动态地填充方法 // 目前还不支持向动态创建的子类中添加成员变量 Ivar(可以使用关联对象为动态创建的子类添加属性) // 在动态创建的子类中,对于父类中不存在的方法,需要使用 %new 指令修饰 // 如果要实例化动态创建的子类的对象,则可以先使用 %c 指令获取动态创建的子类 // %subclass 可以在 %group 内 %subclass Classname : Superclass <Protocol list>
使用示例:
%subclass MyObject : NSObject -(id)init { self = %orig; [self setSomeValue:@"value"]; return self; } // 以下两个新添加的方法分别作为属性 @property (nonatomic, retain) id someValue 的 getter 和 setter %new -(id)someValue { return objc_getAssociatedObject(self, @selector(someValue)); } %new -(void)setSomeValue:(id)value { objc_setAssociatedObject(self, @selector(someValue), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } %end // end subclass MyObject : NSObject %ctor { MyObject* myObject = [[%c(MyObject) alloc] init]; NSLog(@"myObject.someValue = %@", [myObject someValue]); }
-
%property
指令格式:
// %property 指令可以用于将给定的属性添加到由 %subclass 指令动态创建的子类中(就像开发者使用 @property 将属性添加到普通的 Objective-C 子类中一样) // %property 指令也可以用于在 %hook 指令内向现有类添加给定的属性 // %property 必须在 %subclass 或者 %hook 内 %property (nonatomic|assign|retain|copy|weak|strong|getter|setter) Type name;
-
%end
指令格式:
// %end 与 %group、%hook、%subclass 匹配,用于关闭 group block、hook block、subclass block %end
Top Level
Top 级别的指令不应该存在于 %group
、%hook
、%subclass
中
-
%config
指令格式:
// %config 指令用于设置 Logos 配置标识,有以下 3 组用于配置标识的键值对 // 1.generator 键对应的值有: // MobileSubstrate:生成使用 MobileSubstrate 进行 hook 的代码 // internal:生成仅使用 Objective-C RunTime 内部函数进行 hook 的代码 // 2.warnings 键对应的值有: // none:抑制所有警告 // default:非致命警告 // error:使所有警告都致命 // 3.dump 键对应的值有: // yaml:将内部解析树转储为 YAML 格式 // perl:将内部解析树转储为一种适合作为 perl 源进行处理的格式(已废弃,此特性已被删除) %config(Key=Value);
-
%hookf
指令格式:
// %hookf 指令用于为名为 symbolName 的函数生成一个 hook 函数 // 参数 symbolName 既可以传递函数指针,也可以传递字符串常量 // 如果传递字符串常量给参数 symbolName,则会动态地查找该字符串常量所标识的函数 %hookf(rtype, symbolName, args...) { … }
使用示例:
// 给定函数原型 FILE* fopen(const char * path, const char * mode); // 使用 %hookf 生成一个 fopen 函数的 hook 函数 %hookf(FILE *, fopen, const char * path, const char * mode) { NSLog(@"Hey, we're hooking fopen to deny relative paths!"); if (path[0] != '/') { return NULL; } return %orig; // 调用此函数的原始实现 }
开发者通常会 hook 在运行时解析地址的函数,比如 hook
MGGetBoolAnswer
函数:bool (*orig_MGGetBoolAnswer)(CFStringRef); bool fixed_MGGetBoolAnswer(CFStringRef string) { if (CFEqual(string, CFSTR("StarkCapability"))) { return kCFBooleanTrue; } return orig_MGGetBoolAnswer(string); } %ctor { MSHookFunction(((void *)MSFindSymbol(NULL, "_MGGetBoolAnswer")), (void *)fixed_MGGetBoolAnswer, (void **)&orig_MGGetBoolAnswer); ... }
开发者还可以这样做:
%hookf(bool, "_MGGetBoolAnswer", CFStringRef string) { if (CFEqual(string, CFSTR("StarkCapability"))) { return true; } return %orig; }
-
%ctor
指令格式:
// 生成一个具有默认优先级的匿名构造函数 // 如果在 Tweak 源文件中没有显式定义 %ctor,则 Theos 会自动为 Tweak 源文件生成一个 %ctor 指令,并在其中调用 %init(_ungrouped),以让默认的 hook 会生效 %ctor { … }
使用示例:
%hook SpringBoard -(void)reboot { NSLog(@"你好"); %orig; } %end // end hook SpringBoard // 如果开发者不在 Tweak 源文件中重写 %ctor 指令,则 Theos 默认会在 Tweak 源文件中实现 %ctor 指令,并在 %ctor 指令里初始化 _ungrouped 分组,以让默认的 hook 会生效 // 如果开发者在 Tweak 源文件中重写了 %ctor 指令,但是却没做初始化分组的操作,则会导致 hook 失效 // 例如:这里 %hook 指令无法生效,是因为开发者在这里显式地定义了 %ctor 指令,但是却没有显式地调用 %init 指令,因此 %group(_ungrouped) 指令不起作用 %ctor { // 需要显式地调用 %init 指令 }
-
%dtor
指令格式:
// 生成一个具有默认优先级的匿名析构函数 %dtor { … }
Function Level
Function 级别的指令应该只存在于函数块中
-
%init
指令格式:
// 初始化一个分组(或者默认分组) // 不传递分组名将初始化默认分组 _ungrouped // // 传递 class=expr 参数将在初始化时用给定的表达式 expr 替换类 class // 加号标记 +(类似于 Objective-C 中的类方法)可以拼接在类名的前面,用于在初始化时用给定的表达式替换元类 // 减号标记 -(类似于 Objective-C 中的对象方法)可以拼接在类名的前面,用于在初始化时用给定的表达式替换类 // 如果未指定标记,则默认为减号标记 -,此时替换的是类,并且会从类中派生元类 // // 类名替换对于(不能用作 %hook 指令 Classname 参数的类)特别有用,例如:类名包含字符空格或者字符点的类 %init; %init([<class>=<expr>, …]); %init(Group[, [+|-]<class>=<expr>, …]);
使用示例:
%hook SomeClass -(id)init { return %orig; } %end // end hook SomeClass %ctor { %init(SomeClass=objc_getClass("class with spaces or dots in the name")); }
-
%class
指令格式:
// %class 指令已经被废弃,不要在代码中使用它 // %class 指令之前用于定义一个类,已经被 %c 指令取代,但仍然可用 // %class 指令用于创建一个 Class 类型的变量,并使用默认分组 _ungrouped 初始化它 %class Class;
-
%c
指令格式:
// %c 指令用于在运行时获取类对象或者元类对象 // 如果指定了加号 + 标记,则获取到的是元类对象 // 如果指定了减号 - 标记,则获取到的是类对象 // 如果没有指定标记,则默认为减号标记 - %c([+|-]Class)
-
%orig
指令格式:
// %orig 指令用于调用原始的方法实现 // %orig 指令在 %new 指令修饰的方法中不起作用 // 奇怪的是,%orig 指令可以在通过 %subclass 指令动态创建的子类中工作,因为 MobileSubstrate 会在 hook 时产生一个 supercall 闭包(如果正在被 hook 的类中不存在被 hook 的方法,它会创建一个调用父类对应方法实现的桩 stub) // %orig 指令的参数会被传递给原始的函数,因为 Logos 会自动帮开发者传递 self 和 _cmd,所以开发者传递的参数中不需要包括 self 和 _cmd %orig %orig(arg1, …)
-
%log
指令格式:
// %log 指令用于将方法的参数转储到 syslog 中,类型参数也会被 %log 指令记录下来 %log; %log([(<type>)<expr>, …]);
Logos 补充
-
Logos 的文件扩展名
①
.x
文件将由 Logos 处理,然后预处理和编译为Objective-C
②.xm
文件将由 Logos 处理,然后预处理和编译为Objective-C++
③.xi
文件将首先作为Objective-C
进行预处理,然后由 Logos 处理预处理后的结果,最后再进行编译
④.xmi
文件将首先作为Objective-C++
进行预处理,然后由 Logos 处理预处理后的结果,最后再进行编译注意:
①.xi
文件或者.xmi
文件可以在#define
宏中使用 Logos 指令
② 文件扩展名中的x
,代表该文件支持 Logos 语法 -
跨多个文件拆分 Logos 的 hook 代码
默认情况下,Logos 的预处理器在构建时只处理一个
.xm
文件
但是,可以将 Logos 的 hook 代码拆分为多个文件
首先,主文件必须被重命名为一个.xmi
文件
然后,主文件可以使用#include
指令包含其他.xm
文件
Logos 的预处理器在处理主文件之前会将这些文件添加到主文件中通常,不可能跨多个 Logos 文件初始化 hook 分组(hooking groups),但有一个解决方案可以用于解锁此功能
这是通过将分组的初始化包装在静态方法中来实现的,然后可以从其他文件中调用请看以下代码:
它所做的就是在 SpringBoard 应用程序完成启动时 log 一个消息
它位于一个名为TweakGroup
分组的内部,该TweakGroup
分组在一个名为InitGroup()
的静态函数中初始化// Group.xm #import "Shared.h" %group TweakGroup %hook SpringBoard -(void)applicationDidFinishLaunching:(id)arg1 { %orig; NSLog(@"[Group Test] SpringBoard has finished launching"); } %end // end hook SpringBoard %end // end group TweakGroup extern "C" void InitGroup() { %init(TweakGroup); }
正如你可能已经注意到的,在
Group.xm
的顶部有一个关于Shared.h
头文件的导入
Shared.h
只是一个将被导入到 Logos 主文件中的头文件,以便我们可以在那里调用静态函数InitGroup()
:// Shared.h extern "C" void InitGroup();
最后,可以将
Shared.h
导入到包含构造函数的 Logos 文件中。调用静态函数InitGroup()
将从Group.xm
初始化TweakGroup
分组并执行对-[SpringBoard applicationDidFinishLaunching:]
的 hook:// Tweak.xm #import "Shared.h" %ctor { NSLog(@"[Group Test] Our hook for SpringBoard should show up below this"); InitGroup(); }
如果编译没有错误并且执行正确,则应该会输出两条消息:一条来自构造函数,另一条来自分组内的方法。请记住,这并不适用于不在分组内部的 hook
有几件事情需要注意:
- 因为这在正常的 C 语言中不起作用,所以开发者必须为分组和构造函数使用
.xm
格式的文件 - 因为 Logos 不会提示开发者初始化是否被调用了不止一次,所以开发者必须要自己注意调用初始化的次数
- 因为这在正常的 C 语言中不起作用,所以开发者必须为分组和构造函数使用
关于 logify.pl
-
简介
logify.pl
是一个实用工具(.pl
是 Perl 脚本的扩展名),它接收类的头文件(.h
文件)作为输入,并生成 Cydia Substrate 插件(.xm
文件)作为输出,生成的 Cydia Substrate 插件(.xm
文件)会 hook 该类头文件中声明的所有方法,并在这些方法被调用时打印日志消息。这有助于越狱开发者查看在使用过程中何时调用了某些方法。logify.pl
由 Theos 提供 -
用法
在终端执行以下命令:
# 使用头文件 SomeClassHeader.h 中定义的方法创建一个新的 tweak 源文件(tweak.xm) # 开发者可以使用 tweak.xm 作为新 tweak 项目的基础,或者将生成的代码集成到开发者自己的项目中 logify.pl SomeClassHeader.h > tweak.xm
-
使用示例
开发者可以使用
logify.pl
从一个头文件创建一个 Logos 源文件,该 Logos 源文件将记录该头文件的所有函数。下面是使用logify.pl
生成的一个非常简单的 Logos tweak 的例子给定头文件:
// 如果头文件中 @interface 这一行没有继承自任何的 superclass,则 logify.pl 将不会有任何输出 // 例如,要用 @interface SSDownloadAsset : NSObject,而不是用 @interface SSDownloadAsset @interface SSDownloadAsset : NSObject -(NSString *)finalizedPath; -(NSString *)downloadPath; -(NSString *)downloadFileName; +(id)assetWithURL:(id)url type:(int)type; -(id)initWithURLRequest:(id)urlrequest type:(int)type; -(id)initWithURLRequest:(id)urlrequest; -(id)_initWithDownloadMetadata:(id)downloadMetadata type:(id)type; @end
开发者可以在
$THEOS/bin/
目录中找到logify.pl
,并这样使用它:$THEOS/bin/logify.pl ./SSDownloadAsset.h
输出结果如下所示:
%hook SSDownloadAsset -(NSString *)finalizedPath { %log; NSString* r = %orig; NSLog(@" = %@", r); return r; } -(NSString *)downloadPath { %log; NSString* r = %orig; NSLog(@" = %@", r); return r; } -(NSString *)downloadFileName { %log; NSString* r = %orig; NSLog(@" = %@", r); return r; } +(id)assetWithURL:(id)url type:(int)type { %log; id r = %orig; NSLog(@" = %@", r); return r; } -(id)initWithURLRequest:(id)urlrequest type:(int)type { %log; id r = %orig; NSLog(@" = %@", r); return r; } -(id)initWithURLRequest:(id)urlrequest { %log; id r = %orig; NSLog(@" = %@", r); return r; } -(id)_initWithDownloadMetadata:(id)downloadMetadata type:(id)type { %log; id r = %orig; NSLog(@" = %@", r); return r; } %end // end hook SSDownloadAsset