更新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上