examining the macOS gcc binary

39 points by juliethefoxcoon 8 hours ago on lobsters | 5 comments

if you’ve ever written C(or c++) on a mac, you’ll know that the so-called gcc is just clang in disguise

$ gcc
clang: error: no input files

most people(fairly) assume that this is just an alias, but this is not true! it’s a binary!

now, what is this binary? i naively assumed that it would just be shelling out to clang but it turns out the answer is more interesting than that.

int sub_1000006c4(int arg0, int arg1) {
    r19 = arg1;
    r20 = arg0;
    if (*_mainExecutableToolName.once == -0x1) {
            if (*(int8_t *)*_mainExecutableToolName.toolName == 0x63 && *(int8_t *)(*_mainExecutableToolName.toolName + 0x1) == 0x63 && *(int8_t *)(*_mainExecutableToolName.toolName + 0x2) == 0x0) {
                    r21 = "clang";
            }
            else {
                    if (strcmp(*_mainExecutableToolName.toolName, "llvm-gcc") == 0x0) {
                            r21 = "clang";
                    }
                    else {
                            r21 = *_mainExecutableToolName.toolName;
                            if (strcmp(*_mainExecutableToolName.toolName, "c++") == 0x0 || strcmp(r21, "llvm-g++") == 0x0) {
                                    r21 = "clang++";
                            }
                    }
            }
            xcselect_invoke_xcrun();
    }
    while (true) {
            dispatch_once(_mainExecutableToolName.once, ___block_literal_global);
            if (*(int8_t *)*_mainExecutableToolName.toolName == 0x63) {
                    if (*(int8_t *)(*_mainExecutableToolName.toolName + 0x1) == 0x63) {
                            if (*(int8_t *)(*_mainExecutableToolName.toolName + 0x2) != 0x0) {
                                    if (strcmp(*_mainExecutableToolName.toolName, "llvm-gcc") == 0x0) {
                                            r21 = "clang";
                                    }
                                    else {
                                            if (strcmp(*_mainExecutableToolName.toolName, "c++") != 0x0) {
                                                    r21 = *_mainExecutableToolName.toolName;
                                                    if (strcmp(*_mainExecutableToolName.toolName, "llvm-g++") == 0x0) {
                                                            r21 = "clang++";
                                                    }
                                            }
                                            else {
                                                    r21 = "clang++";
                                            }
                                    }
                                    xcselect_invoke_xcrun();
                            }
                            else {
                                    xcselect_invoke_xcrun();
                            }
                    }
                    else {
                            if (strcmp(*_mainExecutableToolName.toolName, "llvm-gcc") == 0x0) {
                                    r21 = "clang";
                            }
                            else {
                                    if (strcmp(*_mainExecutableToolName.toolName, "c++") != 0x0) {
                                            r21 = *_mainExecutableToolName.toolName;
                                            if (strcmp(*_mainExecutableToolName.toolName, "llvm-g++") == 0x0) {
                                                    r21 = "clang++";
                                            }
                                    }
                                    else {
                                            r21 = "clang++";
                                    }
                            }
                            xcselect_invoke_xcrun();
                    }
            }
            else {
                    if (strcmp(*_mainExecutableToolName.toolName, "llvm-gcc") == 0x0) {
                            r21 = "clang";
                    }
                    else {
                            if (strcmp(*_mainExecutableToolName.toolName, "c++") != 0x0) {
                                    r21 = *_mainExecutableToolName.toolName;
                                    if (strcmp(*_mainExecutableToolName.toolName, "llvm-g++") == 0x0) {
                                            r21 = "clang++";
                                    }
                            }
                            else {
                                    r21 = "clang++";
                            }
                    }
                    xcselect_invoke_xcrun();
            }
            continue;
    }
}

there’s some interesting stuff here(for example special logic for checking if it’s being run as c++ or llvm-gcc specifically), but the key thing we’re interested in is this line of code:

this is a function from libxcselect.dylib, but it’s basically the same thing as running xcrun.(fun fact: the xcrun binary basically just calls this function) if you disassemble any xcode-installed developer tool, you’ll see this exact same binary,meaning all the developer tools that xcode-select installs on macOS are actually just being run through xcrun.1
but that raises the important question: what does xcrun do? i’ve certainly never heard of it before this.
well, checking the manpage we can see that it calls developer tools from the developer tool directory. this gives us an important bit of info: the location of the actual gcc binary that’s being called. thankfully for us, there is a --find command that can give us exactly this.

$ xcrun --find gcc
/Library/Developer/CommandLineTools/usr/bin/gcc

looking in this directory, we can see that:
c++, llvm-gcc, llvm-g++ aren’t in this directory, that’s why they’re hardcoded into the binary itself
cc and clang++ are just alises to clang

but most interestingly, g++ is an alias to an actual gcc binary that’s in the same directory:

well, what does that gcc binary do, then?
opening it in my disassembler, we see that it… shells out to xcrun and calls clang(or clang++ depending on how it’s called)

int EntryPoint(int arg0, int arg1) {
    r22 = arg1;
    r21 = arg0;
    asm { sbfiz      x8, x21, #0x3, #0x20 };
    r0 = malloc(r8 + 0x10);
    if (r0 == 0x0) {
            err(0x47, r1);
    }
    r19 = r0;
    *r0 = "/usr/bin/xcrun";
    r23 = *r22;
    r0 = strlen(r23);
    if (*(int8_t *)(r23 + (r0 - 0x1 << r64)) == 0x2b && *(int8_t *)(r23 + (r0 - 0x2 << r64)) == 0x2b) {
            r8 = "clang++";
    }
    else {
            r8 = "clang";
    }
    r19[0x1] = r8;
    if (r21 >= 0x2) {
            asm { ubfiz      x2, x8, #0x3, #0x20 };
            memcpy(r19 + 0x10, r22 + 0x8, r2);
            r8 = r21;
    }
    else {
            r8 = 0x1;
    }
    *(r19 + r8 * 0x8 + 0x8) = 0x0;
    r0 = execv("/usr/bin/xcrun", r19);
    err(0x47, r1);
    return r0;
}

  1. if you’re wondering why this is, my guess is because of SIP. /usr/bin is a protected directory on macOS and i assume apple can’t update it without requiring a restart, and you can update the xcode tools without having to restart your mac. as for why they dont just add the developer tools directory to path, my guess is that they want the freedom to change this directory whenever they want↩︎