前言
NSLog
作為 iOS開(kāi)發(fā)常用的調(diào)試和日志打印方法,大家都是很熟悉了氢烘,
開(kāi)源社區(qū)也為我們貢獻(xiàn)了很多非常優(yōu)秀的日志框架杭朱,比如OC中大名鼎鼎的CocoaLumberjack
,有興趣的同學(xué)可以移步https://github.com/CocoaLumberjack/CocoaLumberjack
在Swift語(yǔ)言下我們還有另外一種選擇,那就是print
如果要自己做日志監(jiān)控的話(huà)甚脉,就需要就需要自己重定向NSLog
和 print
方法了
網(wǎng)絡(luò)上大概有如下幾種方法
- asl讀取日志(iOS10以后已經(jīng)棄用)
NSLog默認(rèn)的輸出到了系統(tǒng)的 /var/log/syslog這個(gè)文件,asl框架能從syslog中讀取日志勉盅,此種方法對(duì)系統(tǒng)無(wú)侵入佑颇,可惜iOS10后已經(jīng)獲取不到日志 - 采用dup2的重定向方式
NSLog最后重定向的句柄是STDERR,NSLog輸出的日志內(nèi)容菇篡,最終都通過(guò)STDERR句柄來(lái)記錄漩符,使用dup2重定向STDERR句柄一喘,可以將內(nèi)容重定向指定的位置驱还,但是重定向之后 - 采用fishhook方式
采用facebook的開(kāi)源框架 fishhook來(lái)動(dòng)態(tài)替換NSLog
和print
方法
本文選擇基于fishhook的方式捕獲 NSLog 和 print
方法
福利:https://github.com/madaoCN/Supervisor已實(shí)現(xiàn)輕量級(jí)的日志打印,使用fishhook hook了 NSLog 和 print
方法
前期準(zhǔn)備
fishhook原理
網(wǎng)上有很多寫(xiě)的很好的文章凸克,這里就不獻(xiàn)丑啦议蟆,大家自行百度/谷歌哈編譯
swift
源碼
swift已經(jīng)開(kāi)源了,我們可以閱讀源碼來(lái)一窺究竟
項(xiàng)目地址 https://github.com/apple/swift
編譯也很簡(jiǎn)單萎战,就是比較耗時(shí)
brew install cmake ninja
mkdir swift-source
cd swift-source
git clone https://github.com/apple/swift.git
./swift/utils/update-checkout --clone
cd swift
utils/build-script --release-debuginfo
最終的編譯文件可能會(huì)比較大咐容,我的大概是 44.18GB, 編譯前請(qǐng)預(yù)留足夠的空間
搞定了的話(huà),黑喂狗~
NSLog源碼閱讀
NSLog的源碼位置在 你的編譯工程目錄/Swift-Build/swift-corelibs-foundation
下
/* Output from NSLogv is serialized, in that only one thread in a process can be doing
* the writing/logging described above at a time. All attempts at writing/logging a
* message complete before the next thread can begin its attempts. The format specification
* allowed by these functions is that which is understood by NSString's formatting capabilities.
* CFLog1() uses writev/fprintf to write to stderr. Both these functions ensure atomic writes.
*/
public func NSLogv(_ format: String, _ args: CVaListPointer) {
let message = NSString(format: format, arguments: args)
CFLog1(CFLogLevel(kCFLogLevelWarning), message._cfObject)
}
public func NSLog(_ format: String, _ args: CVarArg...) {
withVaList(args) {
NSLogv(format, $0)
}
}
我們可以看到NSLog
調(diào)用
NSLog ------> NSLogv ------> CFLog1
結(jié)合源碼中的注釋CFLog1() uses writev/fprintf to write to stderr
基本可以猜到NSLog
最終會(huì)調(diào)用writev 和 fprintf
方法蚂维,接下來(lái)我們順騰摸瓜看下 CFLog1
的邏輯
void CFLog1(CFLogLevel lev, CFStringRef message) {
#if TARGET_OS_ANDROID
if (message == NULL) message = CFSTR("NULL");
...
CFStringEncoding encoding = kCFStringEncodingUTF8;
CFIndex maxLength = CFStringGetMaximumSizeForEncoding(CFStringGetLength(message), encoding) + 1;
...
if (maxLength == 1) {
// was crashing with zero length strings
// https://bugs.swift.org/browse/SR-2666
strcpy(buffer, " "); // log empty string
}
else
CFStringGetCString(message, buffer, maxLength, encoding);
__android_log_print(priority, tag, "%s", buffer);
// ======= 注意這里 =======
fprintf(stderr, "%s\n", buffer);
if (buffer != &stack_buffer[0]) free(buffer);
#else
// ======= 注意這里 =======
CFLog(lev, CFSTR("%@"), message);
#endif
}
可以看到如果是安卓環(huán)境下戳粒,會(huì)調(diào)用 fprintf
, 否則會(huì)調(diào)用 CFLog
方法
NSLog
↓
NSLogv
↓
CFLog1
↓
CFLog
現(xiàn)在調(diào)用的順序是這樣滴,接下來(lái)往下走, 我們看看CFLog
void CFLog(int32_t lev, CFStringRef format, ...) {
va_list args;
va_start(args, format);
_CFLogvEx3(NULL, NULL, NULL, NULL, lev, format, args, __builtin_return_address(0));
va_end(args);
}
// 調(diào)用了_CFLogvEx3
CF_EXPORT void _CFLogvEx3(CFLogFunc logit, CFStringRef (*copyDescFunc)(void *, const void *), CFStringRef (*contextDescFunc)(void *, const void *, const void *, bool, bool *), CFDictionaryRef formatOptions, int32_t lev, CFStringRef format, va_list args, void *addr) {
_CFLogvEx2Predicate(logit, copyDescFunc, contextDescFunc, formatOptions, lev, format, args, _cf_logging_style_legacy);
}
// 調(diào)用了_CFLogvEx2Predicate
static void _CFLogvEx2Predicate(CFLogFunc logit, CFStringRef (*copyDescFunc)(void *, const void *), CFStringRef (*contextDescFunc)(void *, const void *, const void *, bool, bool *), CFDictionaryRef formatOptions, int32_t lev, CFStringRef format, va_list args, _cf_logging_style loggingStyle) {
#if TARGET_OS_MAC
uintptr_t val = (uintptr_t)_CFGetTSD(__CFTSDKeyIsInCFLog);
if (3 < val) return; // allow up to 4 nested invocations
_CFSetTSD(__CFTSDKeyIsInCFLog, (void *)(val + 1), NULL);
#endif
CFStringRef str = format ? _CFStringCreateWithFormatAndArgumentsAux2(kCFAllocatorSystemDefault, copyDescFunc, contextDescFunc, formatOptions, (CFStringRef)format, args) : 0;
CFIndex blen = str ? CFStringGetMaximumSizeForEncoding(CFStringGetLength(str), kCFStringEncodingUTF8) + 1 : 0;
char *buf = str ? (char *)malloc(blen) : 0;
if (str && buf) {
Boolean converted = CFStringGetCString(str, buf, blen, kCFStringEncodingUTF8);
size_t len = strlen(buf);
// silently ignore 0-length or really large messages, and levels outside the valid range
if (converted && !(len <= 0 || (1 << 24) < len) && !(lev < ASL_LEVEL_EMERG || ASL_LEVEL_DEBUG < lev)) {
if (logit) {
logit(lev, buf, len, 1);
}
else if (loggingStyle == _cf_logging_style_os_log) {
// ======= 注意這里 =======
__CFLogCString(lev, buf, len, 1);
}
else if (loggingStyle == _cf_logging_style_legacy) {
// ======= 注意這里 =======
__CFLogCStringLegacy(lev, buf, len, 1);
}
}
}
if (buf) free(buf);
if (str) CFRelease(str);
#if TARGET_OS_MAC
_CFSetTSD(__CFTSDKeyIsInCFLog, (void *)val, NULL);
#endif
}
會(huì)調(diào)用到 __CFLogCString
和 __CFLogCStringLegacy
這兩個(gè)方法虫啥,那么現(xiàn)在調(diào)用的流程是這樣
NSLog
↓
NSLogv
↓
CFLog1
↓
CFLog
↓
_CFLogvEx3
↓
_CFLogvEx2Predicate
|
/ \
/ \
/ \
__CFLogCString __CFLogCStringLegacy
繼續(xù)閱讀源碼__CFLogCString
和 __CFLogCStringLegacy
這兩個(gè)方法最終都調(diào)用了_logToStderr
方法
static void _logToStderr(char *banner, const char *message, size_t length) {
#if TARGET_OS_MAC
struct iovec v[3];
v[0].iov_base = banner;
v[0].iov_len = banner ? strlen(banner) : 0;
v[1].iov_base = (char *)message;
v[1].iov_len = length;
v[2].iov_base = "\n";
v[2].iov_len = (message[length - 1] != '\n') ? 1 : 0;
int nv = (v[0].iov_base ? 1 : 0) + 1 + (v[2].iov_len ? 1 : 0);
static CFLock_t lock = CFLockInit;
__CFLock(&lock);
// ======= 注意這里 =======
writev(STDERR_FILENO, v[0].iov_base ? v : v + 1, nv);
__CFUnlock(&lock);
#elif TARGET_OS_WIN32
size_t bannerLen = strlen(banner);
size_t bufLen = bannerLen + length + 1;
char *buf = (char *)malloc(sizeof(char) * bufLen);
if (banner) {
// Copy the banner into the debug string
memmove_s(buf, bufLen, banner, bannerLen);
// Copy the message into the debug string
strcpy_s(buf + bannerLen, bufLen - bannerLen, message);
} else {
strcpy_s(buf, bufLen, message);
}
buf[bufLen - 1] = '\0';
fprintf_s(stderr, "%s\n", buf);
// This Win32 API call only prints when a debugger is active
// OutputDebugStringA(buf);
free(buf);
#else
size_t bannerLen = strlen(banner);
size_t bufLen = bannerLen + length + 1;
char *buf = (char *)malloc(sizeof(char) * bufLen);
if (banner) {
// Copy the banner into the debug string
memmove(buf, banner, bannerLen);
// Copy the message into the debug string
strncpy(buf + bannerLen, message, bufLen - bannerLen);
} else {
strncpy(buf, message, bufLen);
}
buf[bufLen - 1] = '\0';
// ======= 注意這里 =======
fprintf(stderr, "%s\n", buf);
free(buf);
#endif
}
可見(jiàn)NSLog
最終都調(diào)用了writev
和fprintf
方法
NSLog
↓
NSLogv
↓
CFLog1
↓
CFLog
↓
_CFLogvEx3
↓
_CFLogvEx2Predicate
|
/ \
/ \
/ \
__CFLogCString __CFLogCStringLegacy
\ /
\ /
\ /
_logToStderr
↓
writev / fprintf
結(jié)果與之前的注釋一致蔚约,那么我們只需要使用 fishhook 對(duì) writev / fprintf
方法進(jìn)行hook就能達(dá)到我們的目的了,那么我們繼續(xù)看看 print
函數(shù)的源碼
print函數(shù)源碼閱讀
print的源碼位置在類(lèi)似 你的編譯工程目錄/Swift-Build/build/Xcode-RelWithDebInfoAssert/swift-macosx-x86_64
具體名字和編譯參數(shù)和機(jī)器有關(guān)
我們很容易就找到了源碼
// ============ print方法1
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方法2
public func print<Target: TextOutputStream>(
_ items: Any...,
separator: String = " ",
terminator: String = "\n",
to output: inout Target
) {
// ======= 注意這里 =======
_print(items, separator: separator, terminator: terminator, to: &output)
}
可見(jiàn)print
方法會(huì)調(diào)用_print
方法
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 源碼
@usableFromInline
@_semantics("optimize.sil.specialize.generic.never")
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.
// Note: _isOptional doesn't work here when T == Any, hence we
// use a more elaborate formulation:
if _openExistential(type(of: value as Any), do: _isOptional) {
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)
}
可見(jiàn)調(diào)用流程如下
print ------> _print ------> _print_unlocked
這里的
TextOutputStreamable
CustomDebugStringConvertible
CustomStringConvertible
//////////////////// CustomStringConvertible
public protocol CustomStringConvertible {
var description: String { get }
}
//////////////////// CustomDebugStringConvertible
public protocol CustomDebugStringConvertible {
var debugDescription: String { get }
}
//////////////////// TextOutputStreamable
public protocol TextOutputStreamable {
/// Writes a textual representation of this instance into the given output
/// stream.
func write<Target: TextOutputStream>(to target: inout Target)
}
等都是協(xié)議, 將 Target
傳入并調(diào)用 Target
的 write
方法
我們回過(guò)頭來(lái)看下函數(shù)名
internal func _print_unlocked<T, TargetStream: TextOutputStream>( _ value: T, _ target: inout TargetStream )
target
是遵循 TextOutputStream
協(xié)議的對(duì)象涂籽,也就是我們之前看到的
_Stdout ()
函數(shù)
////////////// TextOutputStream 協(xié)議
public protocol TextOutputStream {
mutating func _lock()
mutating func _unlock()
/// Appends the given string to the stream.
mutating func write(_ string: String)
mutating func _writeASCII(_ buffer: UnsafeBufferPointer<UInt8>)
}
////////////// _Stdout
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 源代碼
SWIFT_RUNTIME_STDLIB_INTERNAL
__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);
}
我們可以看到
_Stdout -> _swift_stdlib_fwrite_stdout -> fwrite
結(jié)合之前的調(diào)用方法, 最終也調(diào)用了 fwrite
方法
print -> _print -> _print_unlocked -> (print items) -> (write/description.write/debugDescription.write)-> Stdout -> _swift_stdlib_fwrite_stdout -> fwrite
繞了這么大的一圈苹祟,我們得出的結(jié)論是,print
函數(shù)最終調(diào)用了fwrite
中場(chǎng)休息~~~~
最終评雌,如果我們要日志監(jiān)控的話(huà)树枫,只需要hook如下三個(gè)方法
NSLog 調(diào)用 writev / fprintf
print 調(diào)用 fwrite
Hook代碼
- 首先我們hook
writev
, 函數(shù)原型
static ssize_t writev(int a, const struct iovec * v, int v_len);
/// struct iovec 類(lèi)型
struct iovec {
void * iov_base; /* [XSI] Base address of I/O memory region */
size_t iov_len; /* [XSI] Size of region iov_base points to */
};
具體hook代碼
//--------------------------------------------------------------------------
// MARK: hook NSLog
//--------------------------------------------------------------------------
// origin writev IMP
static ssize_t (*orig_writev)(int a, const struct iovec * v, int v_len);
// swizzle method
ssize_t asl_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]];
}
////////// do something 這里可以捕獲到日志 string
// invoke origin mehtod
ssize_t result = orig_writev(a, v, v_len);
return result;
}
- 然后是
fprintf
函數(shù),這里因?yàn)?fprintf 是可變參數(shù)景东,具體可變參數(shù)相關(guān)使用可見(jiàn)博主的另外一篇博客 va_list 可變參數(shù)概覽
這里先使用NSString 的 @selector(initWithFormat : arguments)
方法生成要輸出的字符串砂轻,直接調(diào)用 origin_fprintf
將自行生成的字符串作為參數(shù)就行了,免去再次傳遞可變參數(shù)至 origin_fprintf
//--------------------------------------------------------------------------
// MARK: hook fprintf
//--------------------------------------------------------------------------
// origin fprintf IMP
static int (*origin_fprintf)(FILE * __restrict, const char * __restrict, ...);
// swizzle method
int asl_fprintf(FILE * __restrict file, const char * __restrict format, ...)
{
/*
typedef struct {
unsigned int gp_offset;
unsigned int fp_offset;
void *overflow_arg_area;
void *reg_save_area;
} va_list[1];
*/
va_list args;
va_start(args, format);
NSString *formatter = [NSString stringWithUTF8String:format];
NSString *string = [[NSString alloc] initWithFormat:formatter arguments:args];
////////// do something 這里可以捕獲到日志
// invoke orign fprintf
int result = origin_fprintf(file, [string UTF8String]);
va_end(args);
return result;
}
- 然后是
fprintf
方法
調(diào)用例如 fprintf ("test");
方法時(shí)候 asl_fwrite
會(huì)調(diào)用兩次斤吐,參數(shù)一次是test
,另一次是\n
搔涝,所以先將字符串放入__messageBuffer
,等收到\n
時(shí),再將 __messageBuffer
中轉(zhuǎn)成字符串一次性讀取
//--------------------------------------------------------------------------
// MARK: hook print for swift
//--------------------------------------------------------------------------
// origin fwrite IMP
static size_t (*orig_fwrite)(const void * __restrict, size_t, size_t, FILE * __restrict);
static char *__messageBuffer = {0};
static int __buffIdx = 0;
void reset_buffer()
{
__messageBuffer = calloc(1, sizeof(char));
__messageBuffer[0] = '\0';
__buffIdx = 0;
}
// swizzle method
size_t asl_fwrite(const void * __restrict ptr, size_t size, size_t nitems, FILE * __restrict stream) {
if (__messageBuffer == NULL) {
// initial Buffer
reset_buffer();
}
char *str = (char *)ptr;
NSString *s = [NSString stringWithCString:str encoding:NSUTF8StringEncoding];
if (__messageBuffer != NULL) {
if (str[0] == '\n' && __messageBuffer[0] != '\0') {
s = [[NSString stringWithCString:__messageBuffer encoding:NSUTF8StringEncoding] stringByAppendingString:s];
// reset buffIdx
reset_buffer();
////////// do something 這里可以捕獲到日志
}
else {
// append buffer
__messageBuffer = realloc(__messageBuffer, sizeof(char) * (__buffIdx + nitems + 1));
for (size_t i = __buffIdx; i < nitems; i++) {
__messageBuffer[i] = str[i];
__buffIdx ++;
}
__messageBuffer[__buffIdx + 1] = '\0';
__buffIdx ++;
}
}
return orig_fwrite(ptr, size, nitems, stream);
}
最后就是 hook 的代碼曲初,沒(méi)啥好說(shuō)的
//--------------------------------------------------------------------------
// MARK: fishhook調(diào)用
//--------------------------------------------------------------------------
// hook writev
rebind_symbols((struct rebinding[1]){{
"writev",
asl_writev,
(void*)&orig_writev
}}, 1);
// hook fwrite
rebind_symbols((struct rebinding[1]){{
"fwrite",
asl_fwrite,
(void *)&orig_fwrite}}, 1);
// hook fprintf
rebind_symbols((struct rebinding[1]){{
"fprintf",
asl_fprintf,
(void *)&origin_fprintf}}, 1);
接下來(lái)我們看下成果
具體的代碼体谒,請(qǐng)見(jiàn)https://github.com/madaoCN/Supervisor 功能還在完善中,將間斷更新
參考
用fishhook hook輸出方法(NSLog, print)
捕獲NSLog日志小記
GodEye日志監(jiān)控
iOS逆向工程 - fishhook原理