检测iOS应用程序是否在调试器中运行

57

我已经设置了我的应用程序,可以将调试输出发送到控制台或日志文件。现在,我想在代码中决定:

  • 它是在调试器(或模拟器)中运行,并且有一个控制台窗口,我想直接读取输出,还是
  • 没有控制台窗口,因此输出应该被重定向到文件。

是否有一种方法可以确定应用程序是否在调试器中运行?


请查看以下答案:https://dev59.com/hXRB5IYBdhLWcg3w9Lvn - fsaint
2
@Felz (@angrest): 一个程序可以在设备上运行,同时还在调试器中。 - kennytm
@KennyTM 是完全正确的。在设备上进行调试,你将会得到一个控制台。 - fsaint
8个回答

50

苹果公司提供了一个函数来检测程序是否正在被调试,可以在 Technical Q&A 1361 中找到该函数的相关内容(Mac 库中的条目iOS 库中的条目;它们是相同的)。

以下是 Technical Q&A 中的代码:

#include <assert.h>
#include <stdbool.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/sysctl.h>

static bool AmIBeingDebugged(void)
    // Returns true if the current process is being debugged (either 
    // running under the debugger or has a debugger attached post facto).
{
    int                 junk;
    int                 mib[4];
    struct kinfo_proc   info;
    size_t              size;

    // Initialize the flags so that, if sysctl fails for some bizarre 
    // reason, we get a predictable result.

    info.kp_proc.p_flag = 0;

    // Initialize mib, which tells sysctl the info we want, in this case
    // we're looking for information about a specific process ID.

    mib[0] = CTL_KERN;
    mib[1] = KERN_PROC;
    mib[2] = KERN_PROC_PID;
    mib[3] = getpid();

    // Call sysctl.

    size = sizeof(info);
    junk = sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0);
    assert(junk == 0);

    // We're being debugged if the P_TRACED flag is set.

    return ( (info.kp_proc.p_flag & P_TRACED) != 0 );
}

还要注意以下这篇问答末尾的说明:

重要提示:由于kinfo_proc结构体(在<sys/sysctl.h>中)的定义是由__APPLE_API_UNSTABLE进行条件编译的,因此您应该将上述代码的使用限制为您程序的调试版本。


7
在那份文件中,有一件事我不太明白……在代码后面它说:“由于 kinfo_proc 结构体的定义(在 <sys/sysctl.h> 中)已经被 __APPLE_API_UNSTABLE 宏定义所限制,你应该将上述代码的使用限制在程序的 debug 版本中。” 因此,如果我只能将它包含在调试版本中,这个代码有什么意义呢?如果我只能在调试版本中使用它……这意味着我已经知道自己在使用调试版本了吧…… - Andrea
5
这个问题不是关于“构建”是否是“调试版”,而是关于应用程序是否在“调试器”中运行。该问题已经假定了进行了调试版本的构建和运行,然后只想决定如何处理日志消息。您说得对,文档实际上表示您不应该在向客户提供的发布版本中使用此技术。 - DarkDust
感谢您的澄清。我希望能够使用您的建议来解决一个类似的问题(https://dev59.com/kmjWa4cB1Zd3GeqPqWgD)。不幸的是,看起来它并不是正确的解决方案... - Andrea
1
看起来kinfo_proc不再由__APPLE_API_UNSTABLE条件编译了,所以我希望它不仅在调试中使用是可以的。 - silyevsk
有任何想法如何将其实现到Swift应用程序委托中吗? - Konstantinos Natsios

18

调试器可以在启动要调试的进程时设置环境变量。在Xcode中,可以通过转到菜单项Product->Edit Scheme来实现此操作。然后,在Debug方案的Arguments选项卡下添加一个新的环境变量。该变量的名称应为“debugger”,值为“true”。接下来,可以使用以下代码片段来确定调试器是否启动了您的进程:

NSDictionary* env = [NSProcessInfo processInfo].environment;

if ([env[@"debugger"] isEqual:@"true"]) {
    NSLog(@"debugger yes");
}
else {
    NSLog(@"debugger no");
}

好吧,那似乎不是一个解决方案 - 当应用程序在连接到XCode/调试器的iPhone上运行时 - 我该如何检查XCode所在机器的环境变量? - Axel
1
调试器设置了它启动的进程的环境,可以在模拟器或设备上进行。在两种情况下,调试器都可以添加环境变量。我在设备和模拟器上测试过,都能正常工作。这些环境变量不是针对主机机器设置的,而是针对在iOS上运行的应用程序设置的。 - Evan
@Evan 即使过去这个方法可行,但至少在xcode 4.5中已经不再适用。没有这样的环境变量。 - Tertium
1
@Tertium,我已经更新了答案,指出了在Xcode中设置环境变量的正确位置。 - Evan
谢谢,我明天会尝试并回复。 - Tertium

17

对于那些寻求更简单解决方案的人 - 这个解决方案非常完美:

func isDebuggerAttached() -> Bool {
    return getppid() != 1
}

1
这正是许多反调试器检测工具所使用的。 - Dylan Nicholson
1
太棒了,谢谢!在Xcode 13.2.1上仍然有效。 - meaning-matters

10

最简单的解决方案实际上是

_isDebugging = isatty(STDERR_FILENO);

虽然它并不完全等同于告诉我们应用程序是否在调试器下运行,但足够好(甚至更好?)以确定是否应将日志写入磁盘。


8

基于一个重复的帖子中的答案,该答案同样适用于Objective-C,展示了HockeyApp-iOS如何实现, 这是Swift 5版本:

let isDebuggerAttached: Bool = {
    var debuggerIsAttached = false

    var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
    var info: kinfo_proc = kinfo_proc()
    var info_size = MemoryLayout<kinfo_proc>.size

    let success = name.withUnsafeMutableBytes { (nameBytePtr: UnsafeMutableRawBufferPointer) -> Bool in
        guard let nameBytesBlindMemory = nameBytePtr.bindMemory(to: Int32.self).baseAddress else { return false }
        return -1 != sysctl(nameBytesBlindMemory, 4, &info/*UnsafeMutableRawPointer!*/, &info_size/*UnsafeMutablePointer<Int>!*/, nil, 0)
    }

    // The original HockeyApp code checks for this; you could just as well remove these lines:
    if !success {
        debuggerIsAttached = false
    }

    if !debuggerIsAttached && (info.kp_proc.p_flag & P_TRACED) != 0 {
        debuggerIsAttached = true
    }

    return debuggerIsAttached
}()

这段代码对我来说不起作用。你能解释一下它是如何工作的吗? - Vijay Kharage

6

总是很好有不同的解决方案,所以这是我的建议:

想法是检查stderr文件句柄(这是NSLog打印到的地方)。自iOS 4以来,这个解决方案一直可靠地工作,并在模拟器和设备上持续运行,包括iOS 9。

#import <sys/ioctl.h>
#import <sys/param.h>
#if TARGET_IPHONE_SIMULATOR
    #import <sys/conf.h>
#else
// Not sure why <sys/conf.h> is missing on the iPhoneOS.platform.
// It's there on iPhoneSimulator.platform, though. We need it for D_DISK, only:
    #if ! defined(D_DISK)
        #define D_DISK  2
    #endif
#endif

BOOL isDebuggerAttatchedToConsole(void)
{
    // We use the type of the stderr file descriptor
    // to guess if a debugger is attached.

    int fd = STDERR_FILENO;

    // is the file handle open?
    if (fcntl(fd, F_GETFD, 0) < 0) {
        return NO;
    }

    // get the path of stderr's file handle
    char buf[MAXPATHLEN + 1];
    if (fcntl(fd, F_GETPATH, buf ) >= 0) {
        if (strcmp(buf, "/dev/null") == 0)
            return NO;
        if (strncmp(buf, "/dev/tty", 8) == 0)
            return YES;
    }

    // On the device, without attached Xcode, the type is D_DISK (otherwise it's D_TTY)
    int type;
    if (ioctl(fd, FIODTYPE, &type) < 0) {
        return NO;
    }

    return type != D_DISK;
}

-1

我通常会选择更简单的解决方案;二进制文件是否使用了优化编译?

调试版本没有进行优化,而且日志很好用。发布版本应该进行了优化,并且不需要那么多日志。您可以通过检查__OPTIMIZE__符号来确定这一点。

对于日志记录,我使用这个设置来记录函数:

#ifdef __OPTIMIZE__ 
  #define CWLog(...)
  #define CWLogDebug(...)
  #define CWLogInfo(...)
#else
  #define CWLog(...) NSLog(__VA_ARGS__)
  #define CWLogDebug( s, ... ) NSLog( @"DEBUG <%p %@:(%d)> %@", self, [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, [NSString stringWithFormat:(s), ##__VA_ARGS__] )
  #ifndef LOG_INFO
    #define CWLogInfo(...)
  #else
    #define CWLogInfo( s, ... ) NSLog( @"INFO <%p %@:(%d)> %@", self, [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, [NSString stringWithFormat:(s), ##__VA_ARGS__] )
  #endif
#endif
#define CWLogWarning( s, ... ) NSLog( @"WARNING <%p %@:(%d)> %@", self, [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, [NSString stringWithFormat:(s), ##__VA_ARGS__] )
#define CWLogError( s, ... ) NSLog( @"ERROR <%p %@:(%d)> %@", self, [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, [NSString stringWithFormat:(s), ##__VA_ARGS__] )

3
提示:调试版本有时会被优化。您可以更改构建设置来解决这个问题。只要开发人员没有将其从默认设置更改,您的答案就是有效的。;-) - Constantino Tsarouhas
11
抱歉,这个答案没有理解问题的要点。发帖人想要根据应用程序是否在调试器中运行将调试消息输出到控制台或文件,而不是检查是否是调试“构建”或不是。请注意保持原意,并尽可能使翻译易于理解。 - DarkDust

-10
为什么不在Swift中使用条件编译块?
     #if DEBUG
        // Do something.
     #endif

有任何异议吗?

你可以定义是否需要运行时常量

#if DEBUG
public let IS_RUNNING_IN_DEBUGGER: Bool = true
#else
public let IS_RUNNING_IN_DEBUGGER: Bool = false
#endif

相同的方法可以在Objc和更多领域中使用。


然而奇怪的是,当我直接在设备上启动我的DEBUG构建可执行文件而不是从Xcode开始时,它仍然返回true... - Dylan Nicholson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接