更新2021/2/26(感谢@lgq_9b65的提醒, 由于我一直没用真机测试, 才搞出这个乌龙.)
真机测试中发现以下问题
-
NSLog
没有调用writev
-
print
没有调用fwrite
由于暂时没有找到真机底层调用方法, 所以删除了fishhook
, 使用dup2 + pipe
来重定向输出
相关代码如下:
let stdoutPipe = [[NSPipe alloc] init];
let stderrPipe = [[NSPipe alloc] init];
// 由于真机再断开数据线后会输出到 /dev/null 中, 这里要手动将buff设置为unbuffered
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
// 保留原始的fileno, 用于之后重新输出到控制台
int ori_stdout_fileNo = dup(STDOUT_FILENO);
int ori_stderr_fileNo = dup(STDERR_FILENO);
dup2(stdoutPipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO);
dup2(stderrPipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO);
stdoutPipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle * _Nonnull handle) {
NSData *data = handle.availableData;
NSString *str = [[NSString alloc] initWithData:data encoding:(NSUTF8StringEncoding)];
[[logInWindowManager share] addPrintWithMessage:str];
const char * utf8Str = str.UTF8String;
// 将数据重新写入到原始fileno中
write(ori_stdout_fileNo,utf8Str,strlen(utf8Str));
};
stderrPipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle * _Nonnull handle) {
NSData *data = handle.availableData;
NSString *str = [[NSString alloc] initWithData:data encoding:(NSUTF8StringEncoding)];
[[logInWindowManager share] addPrintWithMessage:str];
const char * utf8Str = str.UTF8String;
// 将数据重新写入到原始fileno中
write(ori_stderr_fileNo,utf8Str,strlen(utf8Str));
};
用这种方法也有一些问题
- 无法分割每一条数据, 都是混到一起的
以下为原文
初衷
一直以来做项目都是手机连电脑, 然后在控制台查看log信息, 中午吃饭突然想拿出手机看下项目, 但是在食堂没有电脑, 没法看log, 所以心血来潮, 想把log信息显示在window上.
开搞
闲话不多说! UI方面没什么可说的, 就是一个简单的Window+UITextView, 重点是怎么把log信息获取到?首先想到的就是像Runtime 一样吧NSLog
方法hook到, 然后google了一下发现个好东西fishhook, 下边是他的用法:
#import <dlfcn.h>
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "fishhook.h"
static int (*orig_close)(int);
static int (*orig_open)(const char *, int, ...);
int my_close(int fd) {
printf("Calling real close(%d)\n", fd);
return orig_close(fd);
}
int my_open(const char *path, int oflag, ...) {
va_list ap = {0};
mode_t mode = 0;
if ((oflag & O_CREAT) != 0) {
// mode only applies to O_CREAT
va_start(ap, oflag);
mode = va_arg(ap, int);
va_end(ap);
printf("Calling real open('%s', %d, %d)\n", path, oflag, mode);
return orig_open(path, oflag, mode);
} else {
printf("Calling real open('%s', %d)\n", path, oflag);
return orig_open(path, oflag, mode);
}
}
int main(int argc, char * argv[])
{
@autoreleasepool {
rebind_symbols((struct rebinding[2]){{"close", my_close, (void *)&orig_close}, {"open", my_open, (void *)&orig_open}}, 2);
// Open our own binary and print out first 4 bytes (which is the same
// for all Mach-O binaries on a given architecture)
int fd = open(argv[0], O_RDONLY);
uint32_t magic_number = 0;
read(fd, &magic_number, 4);
printf("Mach-O Magic Number: %x \n", magic_number);
close(fd);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
稳了! 很符合预期嘛~首先用类似的方法尝试hook NSLog
// orig_NSLog是原有方法被替换后 把原来的实现方法放到另一个地址中
// new_NSLog就是替换后的方法了
static void (*orig_NSLog)(NSString *format, ...);
void(new_NSLog)(NSString *format, ...) {
va_list args;
if(format) {
va_start(args, format);
NSString *message = [[NSString alloc] initWithFormat:format arguments:args];
[[logInWindowManager share] addPrintWithMessage:message needReturn:true];
orig_NSLog(@"%@", message);
va_end(args);
}
}
...
// 初始化方法里进行替换
rebind_symbols((struct rebinding[1]){{"NSLog", new_NSLog, (void *)&orig_NSLog}}, 1);
看一下运行效果
DDLog
本来到这里就应该结束了的, 不过看了一下自己项目里, 发现项目里用的是都是DDLog, 这就尴尬了.所以咱们来看一下他的代码.所有的宏定义都汇聚到下面这个方法上:
/**
* Logging Primitive.
*
* This method is used by the macros or logging functions.
* It is suggested you stick with the macros as they're easier to use.
*
* @param asynchronous YES if the logging is done async, NO if you want to force sync
* @param level the log level
* @param flag the log flag
* @param context the context (if any is defined)
* @param file the current file
* @param function the current function
* @param line the current code line
* @param tag potential tag
* @param format the log format
*/
+ (void)log:(BOOL)asynchronous
level:(DDLogLevel)level
flag:(DDLogFlag)flag
context:(NSInteger)context
file:(const char *)file
function:(const char *)function
line:(NSUInteger)line
tag:(id)tag
format:(NSString *)format, ... NS_FORMAT_FUNCTION(9,10);
经过一系列的找, 找到下面的方法(截取了一部分)
- (void)lt_log:(DDLogMessage *)logMessage {
...
if (_numProcessors > 1) {
for (DDLoggerNode *loggerNode in self._loggers) {
if (!(logMessage->_flag & loggerNode->_level)) {
continue;
}
dispatch_group_async(_loggingGroup, loggerNode->_loggerQueue, ^{ @autoreleasepool {
[loggerNode->_logger logMessage:logMessage];
} });
}
dispatch_group_wait(_loggingGroup, DISPATCH_TIME_FOREVER);
} else {
for (DDLoggerNode *loggerNode in self._loggers) {
if (!(logMessage->_flag & loggerNode->_level)) {
continue;
}
dispatch_sync(loggerNode->_loggerQueue, ^{ @autoreleasepool {
[loggerNode->_logger logMessage:logMessage];
} });
}
}
...
}
loggerNode->_logger
是一个协议 遵守这个协议的一共有5个, 其中只有DDTTYLogger
负责输出到控制台找到他实现的代理方法, 同样是截取了一部分
- (void)logMessage:(DDLogMessage *)logMessage {
...
int iovec_len = (_automaticallyAppendNewlineForCustomFormatters) ? 5 : 4;
struct iovec v[iovec_len];
if (colorProfile) {
v[0].iov_base = colorProfile->fgCode;
v[0].iov_len = colorProfile->fgCodeLen;
v[1].iov_base = colorProfile->bgCode;
v[1].iov_len = colorProfile->bgCodeLen;
v[iovec_len - 1].iov_base = colorProfile->resetCode;
v[iovec_len - 1].iov_len = colorProfile->resetCodeLen;
} else {
v[0].iov_base = "";
v[0].iov_len = 0;
v[1].iov_base = "";
v[1].iov_len = 0;
v[iovec_len - 1].iov_base = "";
v[iovec_len - 1].iov_len = 0;
}
v[2].iov_base = (char *)msg;
v[2].iov_len = msgLen;
if (iovec_len == 5) {
v[3].iov_base = "\n";
v[3].iov_len = (msg[msgLen] == '\n') ? 0 : 1;
}
writev(STDERR_FILENO, v, iovec_len);
...
}
从这里可以看到他最终调了writev
这个方法那么接下来同样的方法hook他
static ssize_t (*orig_writev)(int a, const struct iovec * v, int v_len);
ssize_t new_writev(int a, const struct iovec *v, int v_len) {
NSMutableString *string = [NSMutableString string];
for (int i = 0; i < v_len; i++) {
char *c = (char *)v[i].iov_base;
[string appendString:[NSString stringWithCString:c encoding:NSUTF8StringEncoding]];
}
ssize_t result = orig_writev(a, v, v_len);
dispatch_async(dispatch_get_main_queue(), ^{
[[logInWindowManager share] addPrintWithMessage:string needReturn:false];
});
return result;
}
...
rebind_symbols((struct rebinding[1]){{"writev", new_writev, (void *)&orig_writev}}, 1);
再运行的时候 发现 NSLog
的底层调用也是调用了writev
方法, 所以上边hook的NSLog
就可以先注释掉了
看一下效果:
这回附加的信息也都出来了, 完美!!
Swift?
原文是以Swift3为例子, 后续添加了一些Swift5的更新
这回到这里该结束了吧....又来需求了... 项目里还有一些swift文件怎么办?本来想像hookC方法那样hook print
结果swift获取不到函数指针google上找到一篇文章: Function hooking in Swift, 按照文章的说明, clone下来rd_route满心欢喜的写demo测试一下, 结果......
[图片上传失败...(image-2f691c-1610858461950)]
没办法了, 想了一上午, 突然想知道print
方法内部实现是什么样的??
立马开搞!, Swift已经开源了正好看一下源码.
按照这篇文章How to Read the Swift Standard Library Source步骤, 编译完成打开源码看一下, 首先找到print方法:
@inline(never)
@_semantics("stdlib_binary_only")
public func print(
_ items: Any...,
separator: String = " ",
terminator: String = "\n"
) {
if let hook = _playgroundPrintHook {
var output = _TeeStream(left: "", right: _Stdout())
_print(
items, separator: separator, terminator: terminator, to: &output)
hook(output.left)
}
else {
var output = _Stdout()
_print(
items, separator: separator, terminator: terminator, to: &output)
}
}
print调用了_print
, 再看一下_print
:
@_versioned
@inline(never)
@_semantics("stdlib_binary_only")
internal func _print<Target : TextOutputStream>(
_ items: [Any],
separator: String = " ",
terminator: String = "\n",
to output: inout Target
) {
var prefix = ""
output._lock()
defer { output._unlock() }
for item in items {
output.write(prefix)
_print_unlocked(item, &output)
prefix = separator
}
output.write(terminator)
}
接着_print_unlocked
:
@_versioned
@inline(never)
@_semantics("optimize.sil.specialize.generic.never")
@_semantics("stdlib_binary_only")
internal func _print_unlocked<T, TargetStream : TextOutputStream>(
_ value: T, _ target: inout TargetStream
) {
// Optional has no representation suitable for display; therefore,
// values of optional type should be printed as a debug
// string. Check for Optional first, before checking protocol
// conformance below, because an Optional value is convertible to a
// protocol if its wrapped type conforms to that protocol.
if _isOptional(type(of: value)) {
let debugPrintable = value as! CustomDebugStringConvertible
debugPrintable.debugDescription.write(to: &target)
return
}
if case let streamableObject as TextOutputStreamable = value {
streamableObject.write(to: &target)
return
}
if case let printableObject as CustomStringConvertible = value {
printableObject.description.write(to: &target)
return
}
if case let debugPrintableObject as CustomDebugStringConvertible = value {
debugPrintableObject.debugDescription.write(to: &target)
return
}
let mirror = Mirror(reflecting: value)
_adHocPrint_unlocked(value, mirror, &target, isDebugPrint: false)
}
...
internal struct _Stdout : TextOutputStream {
mutating func _lock() {
_swift_stdlib_flockfile_stdout()
}
mutating func _unlock() {
_swift_stdlib_funlockfile_stdout()
}
mutating func write(_ string: String) {
if string.isEmpty { return }
// 非中文输出走这里
// 如果符合ascii规格
if let asciiBuffer = string._core.asciiBuffer {
defer { _fixLifetime(string) }
_swift_stdlib_fwrite_stdout(
UnsafePointer(asciiBuffer.baseAddress!),
asciiBuffer.count,
1)
return
}
// 中文输出走这里
// 不符合ascii 一个一个输出
for c in string.utf8 {
_swift_stdlib_putchar_unlocked(Int32(c))
}
}
}
// ----- 更新Swift 5.0 -----
internal struct _Stdout: TextOutputStream {
internal init() {}
internal mutating func _lock() {
_swift_stdlib_flockfile_stdout()
}
internal mutating func _unlock() {
_swift_stdlib_funlockfile_stdout()
}
internal mutating func write(_ string: String) {
if string.isEmpty { return }
var string = string
_ = string.withUTF8 { utf8 in
_swift_stdlib_fwrite_stdout(utf8.baseAddress!, 1, utf8.count)
}
}
}
先看一些非中文的情况 _swift_stdlib_fwrite_stdout
Swift5.x版本优化了 _Stdout
实现方式, 不再区分ascii与utf8, 统一都执行utf8的方式调用 _swift_stdlib_fwrite_stdout
方法
SWIFT_RUNTIME_STDLIB_INTERFACE
__swift_size_t swift::_swift_stdlib_fwrite_stdout(const void *ptr,
__swift_size_t size,
__swift_size_t nitems) {
return fwrite(ptr, size, nitems, stdout);
}
只是调了fwrite
, 那么咱么只需要hook这个方法就行了.
static size_t (*orig_fwrite)(const void * __restrict, size_t, size_t, FILE * __restrict);
size_t new_fwrite(const void * __restrict ptr, size_t size, size_t nitems, FILE * __restrict stream) {
char *str = (char *)ptr;
__block NSString *s = [NSString stringWithCString:str encoding:NSUTF8StringEncoding];
[[logInWindowManager share] addPrintWithMessage:s needReturn:false];
return orig_fwrite(ptr, size, nitems, stream);
}
下面都是Swift3.x的处理, 可以忽略了.
上边是非中文的情况, 下面看一下中文的情况
SWIFT_RUNTIME_STDLIB_INTERFACE
int swift::_swift_stdlib_putchar_unlocked(int c) {
#if defined(_WIN32)
return _putc_nolock(c, stdout);
#else
return putchar_unlocked(c); // 手机/ 模拟器走这里
#endif
}
...
#define putchar_unlocked(x) putc_unlocked(x, stdout)
...
#define putc_unlocked(x, fp) __sputc(x, fp)
...
#if defined(__GNUC__) && defined(__STDC__)
__header_always_inline int __sputc(int _c, FILE *_p) {
if (--_p->_w >= 0 || (_p->_w >= _p->_lbfsize && (char)_c != '\n'))
return (*_p->_p++ = _c);
else
return (__swbuf(_c, _p));
}
#else
...
// 最后会调用这个
int __swbuf(int, FILE *);
hook掉__swbuf
和fwrite
分别看一下hook到的是什么样的
static size_t (*orig_fwrite)(const void * __restrict __ptr, size_t __size, size_t __nitems, FILE * __restrict __stream);
size_t new_fwrite(const void * __restrict __ptr, size_t __size, size_t __nitems, FILE * __restrict __stream) {
// 这里的_ptr就是传进来的字符
char *chars = (char*)_ptr;
return orig_fwrite(__ptr, __size, __nitems, __stream);
}
static int (*orin___swbuf)(int, FILE *);
int new___swbuf(int c, FILE *p) {
// 这里的c也是传进来的字符
char cChar = (char)c;
return orin___swbuf(c, p);
}
...
print("北京欢迎你aaaaasdfsdfg *^(*&R()8y23rkvwd")
这里是这样的: 输出的字符串有中文也有别的字符, 当是中文时, 因为一个中文等于多个字符, 所以要把__swbuf
连续几次传过来的c合成成一个中文再配合fwrite
的非中文合到一起再输出
下边是我想到的办法, 如果有更好的办法请告诉我, 谢谢!
static char *__chineseChar = {0};
static int __buffIdx = 0;
static NSString *__syncToken = @"token";
static size_t (*orig_fwrite)(const void * __restrict __ptr, size_t __size, size_t __nitems, FILE * __restrict __stream);
size_t new_fwrite(const void * __restrict __ptr, size_t __size, size_t __nitems, FILE * __restrict __stream) {
char *str = (char *)__ptr;
__block NSString *s = [NSString stringWithCString:str encoding:NSUTF8StringEncoding];
dispatch_async(dispatch_get_main_queue(), ^{
@synchronized (__syncToken) {
if (str[0] == '\n' && __chineseChar[0] != '\0') {
s = [[NSString stringWithCString:__chineseChar encoding:NSUTF8StringEncoding] stringByAppendingString:s];
__buffIdx = 0;
__chineseChar = calloc(1, sizeof(char));
}
}
[[logInWindowManager share] addPrintWithMessage:s needReturn:false];
});
return orig_fwrite(__ptr, __size, __nitems, __stream);
}
static int (*orin___swbuf)(int, FILE *);
int new___swbuf(int c, FILE *p) {
@synchronized (__syncToken) {
__chineseChar = realloc(__chineseChar, sizeof(char) * (__buffIdx + 2));
__chineseChar[__buffIdx] = (char)c;
__chineseChar[__buffIdx + 1] = '\0';
__buffIdx++;
}
return orin___swbuf(c, p);
}
总结
代码都不是很难懂, 主要是分享一下我解决问题的过程.源码在我的Github上