11

I'm trying to implement custom asset macro (similar to what assert.h has), but I want to be able to continue execution after I get and assert.

For example, one such ASSERT implementation could be:

#define ASSERT(expr) ((void)( (!!(expr)) || (__debugbreak(), 0)))

__debugbreak is an intrinsic function in Microsoft compilers that inserts software breakpoint, equivalent to _asm int 3 in x86. for iOS there are different ways to implement that __debugbreak:

  • __asm__("int $3"); for x86.
  • __asm__("bkpt #0"); for regular arm.
  • __asm__("brk #0"); for arm64
  • __builtin_trap()
  • raise(SIGTRAP)

but with all of them when my assert hits I cannot simply step over and continue the way I can do when working with visual studio; when something assert in my iOS builds it gets stuck at the assert and I have no choice but to terminate, I cannot even move instruction pointer manually and skip the assert.

Is it possible to implement asserts on iOS that would break into debugger and would still allow me to continue execution?

Pavel P
  • 15,789
  • 11
  • 79
  • 128

1 Answers1

10

Turns out I can achieve what I want by making a syscall:

#include <unistd.h>

#if defined(__APPLE__) && defined(__aarch64__)
#define __debugbreak() __asm__ __volatile__(            \
    "   mov    x0, %x0;    \n" /* pid                */ \
    "   mov    x1, #0x11;  \n" /* SIGSTOP            */ \
    "   mov    x16, #0x25; \n" /* syscall 37 = kill  */ \
    "   svc    #0x80       \n" /* software interrupt */ \
    "   mov    x0, x0      \n" /* nop                */ \
    ::  "r"(getpid())                                   \
    :   "x0", "x1", "x16", "memory")
#elif defined(__APPLE__) && defined(__arm__)
#define __debugbreak() __asm__ __volatile__(            \
    "   mov    r0, %0;     \n" /* pid                */ \
    "   mov    r1, #0x11;  \n" /* SIGSTOP            */ \
    "   mov    r12, #0x25; \n" /* syscall 37 = kill  */ \
    "   svc    #0x80       \n" /* software interrupt */ \
    "   mov    r0, r0      \n" /* nop                */ \
    ::  "r"(getpid())                                   \
    :   "r0", "r1", "r12", "memory")
#elif defined(__APPLE__) && (defined(__i386__) || defined(__x86_64__))
#define __debugbreak() __asm__ __volatile__("int $3; mov %eax, %eax")
#endif

#define MYASSERT(expr) do { if (!(expr)){ __debugbreak(); } } while(0)

There is a trailing NOP mov x0, x0 for a reason: when assert breaks, debugger will stop exactly at the assert line and not some random line where the following instruction happens to be located.

In case if somebody is looking for equivalent of IsDebuggerPresent on iOS, you can use AmIBeingDebugged.

Pavel P
  • 15,789
  • 11
  • 79
  • 128
  • 1
    As far as I can tell, this is missing something important: the system call to make needs to go into `r12` before calling `svc #0x80` (which is the ["software interrupt" to call a kernel method](https://www.theiphonewiki.com/wiki/Kernel_Syscalls)). So it seems that by pure chance you got a value in `r12` that's useful for you, maybe `ptrace`. I recommend you look into `r12` in your working version. – DarkDust May 23 '17 at 20:42
  • Yes, I think you are right, my comment in AmIBeingDebugged thread isn't valid. I figured that `svc #80` simply by inspecting live assembly without even checking what params are needed. – Pavel P May 23 '17 at 21:52
  • For the emulator you need to check for x86_64 as well. – adamfowlerphoto May 24 '17 at 14:11