macOSのシステムコールメモ

macOSシステムコールメモ

macOSシステムコール番号は https://opensource.apple.com/source/xnu/xnu-4903.221.2/bsd/kern/syscalls.master.auto.html に書かれているが,64-bitではこの通りにsyscallしてもbus error となってしまう。

実際には,システムコール番号に0x2000000を足さないといけない。

つまり,mov rax, 1 のようなコードをmov rax, 0x2000001 のようにしないといけない。

その理由は, https://opensource.apple.com/source/xnu/xnu-4903.221.2/osfmk/mach/i386/syscall_sw.h.auto.html の以下のコメントで説明されている。

/*
 * Syscall classes for 64-bit system call entry.
 * For 64-bit users, the 32-bit syscall number is partitioned
 * with the high-order bits representing the class and low-order
 * bits being the syscall number within that class.
 * The high-order 32-bits of the 64-bit syscall number are unused.
 * All system classes enter the kernel via the syscall instruction.
 *
 * These are not #ifdef'd for x86-64 because they might be used for
 * 32-bit someday and so the 64-bit comm page in a 32-bit kernel
 * can use them.
 */

64-bitでの32-bitのシステムコール番号は上位ビットのclassと下位ビットのclass内のシステムコール番号で分割される。

classの構成は,先ほどのsyscall_sw.h内のマクロ定義にある。

#define SYSCALL_CLASS_SHIFT 24
#define SYSCALL_CLASS_MASK (0xFF << SYSCALL_CLASS_SHIFT)
#define SYSCALL_NUMBER_MASK    (~SYSCALL_CLASS_MASK)

#define    I386_SYSCALL_CLASS_MASK     SYSCALL_CLASS_MASK
#define    I386_SYSCALL_ARG_BYTES_SHIFT    (16)
#define    I386_SYSCALL_ARG_DWORDS_SHIFT   (I386_SYSCALL_ARG_BYTES_SHIFT + 2)
#define    I386_SYSCALL_ARG_BYTES_NUM  (64) /* Must be <= sizeof(uu_arg) */
#define    I386_SYSCALL_ARG_DWORDS_MASK    ((I386_SYSCALL_ARG_BYTES_NUM >> 2) -1)
#define    I386_SYSCALL_ARG_BYTES_MASK (((I386_SYSCALL_ARG_BYTES_NUM -1)&~0x3) << I386_SYSCALL_ARG_BYTES_SHIFT)
#define    I386_SYSCALL_NUMBER_MASK    (0xFFFF)

#define SYSCALL_CLASS_NONE 0  /* Invalid */
#define SYSCALL_CLASS_MACH 1  /* Mach */  
#define SYSCALL_CLASS_UNIX 2  /* Unix/BSD */
#define SYSCALL_CLASS_MDEP 3  /* Machine-dependent */
#define SYSCALL_CLASS_DIAG 4  /* Diagnostics */
#define SYSCALL_CLASS_IPC  5  /* Mach IPC */

/* Macros to simpllfy constructing syscall numbers. */
#define SYSCALL_CONSTRUCT_MACH(syscall_number) \
           ((SYSCALL_CLASS_MACH << SYSCALL_CLASS_SHIFT) | \
            (SYSCALL_NUMBER_MASK & (syscall_number)))
#define SYSCALL_CONSTRUCT_UNIX(syscall_number) \
           ((SYSCALL_CLASS_UNIX << SYSCALL_CLASS_SHIFT) | \
            (SYSCALL_NUMBER_MASK & (syscall_number)))
#define SYSCALL_CONSTRUCT_MDEP(syscall_number) \
           ((SYSCALL_CLASS_MDEP << SYSCALL_CLASS_SHIFT) | \
            (SYSCALL_NUMBER_MASK & (syscall_number)))
#define SYSCALL_CONSTRUCT_DIAG(syscall_number) \
           ((SYSCALL_CLASS_DIAG << SYSCALL_CLASS_SHIFT) | \
            (SYSCALL_NUMBER_MASK & (syscall_number)))

exit, read, writeなどのシステムコールUnix-classに含まれるので,SYSCALL_CONSTRUCT_UNIX が使われる。実際にSYSCALL_CONSTRUCT_UNIX(2)

とすると,0x2000002 となる。

macOSでのクロスコンパイラのビルド方法

macOSでのクロスコンパイラのビルド方法について書いていく。

gcc

この記事の執筆時点での最新バージョンのgcc9.1.0をビルドするが,よほど古くない限りは同じ方法でできるはずだ。

gcc-9.1.0のダウンロードと解凍:

$ wget http://ftp.tsukuba.wide.ad.jp/software/gcc/releases/gcc-9.1.0/gcc-9.1.0.tar.gz
$ tar -zxvf gcc-9.1.0.tar.gz && rm -f gcc-9.1.0.tar.gz

そして,gcc-buildディレクトリを作ってconfigureする。ここで,Homebrewでbinutilsをインストールしている場合,ビルドに失敗することがあるため,brew unlink binutilsを実行する。それでもまだ失敗する場合は一時的にbrew uninstall binutilsでアンインストールして,rm -rf ./*gcc-build以下の全てのファイルを削除する(削除しないと,再configureに失敗する)。

$ mkdir gcc-build
$ cd gcc-build

gccのビルドに必要なgmp, mpfr, mpcはいずれもbrew installでインストールできるため,ない場合はインストールする。 gmp, mpfr, mpcのあるディレクトリのPathをbrew --prefix パッケージ名で表示し,そのPathをconfigure時に-with-パッケージ名="ディレクトリのPath"で引数として渡す。

configure:

$ ../gcc-9.1.0/configure --prefix=インストール先 --target="ターゲット名(例: x86_64-elf)" --with-gmp="gmpのPath" --with-mpfr="mpfrのPath" --with-mpc="mpcのPath" AR="/usr/bin/ar" RANLIB="/usr/bin/ranlib"

AR="/usr/bin/ar" RANLIB="/usr/bin/ranlib"の部分は,Homebrewでインストールされたbinutilsが影響を及ぼしている可能性があるため,/usr/bin以下のarとranlibを指定している。

そして,ビルド&インストールを実行する。

このとき,make all-gccに引数として,-jと,その後ろにCPUのコア数を渡すと,CPUに応じてビルドが最適化される。

$ make all-gcc
$ make install-gcc

binutils

binutilsも執筆時点での最新バージョンのbinutils2.32をビルドする。

binutils-2.32のダウンロードと解凍:

$ wget http://ftp.jaist.ac.jp/pub/GNU/binutils/binutils-2.32.tar.gz
$ tar -zxvf binutils-2.32.tar.gz && rm -f binutils-2.32.tar.gz

gccのビルドと同じように,binutils-buildディレクトリを作って,そこでビルドを行う。

$ mkdir binutils-build
$ cd binutils-build

そして,configureを行う。

$ ../binutils-2.32/configure --prefix="インストール先" --target="ターゲット名(例: x86_64-elf)"

ビルドは,gccとは違い,all-binutilsなどつけなくて良い。binutilsのビルドも,make -jコア数でCPUに対して最適化できる。

$ make
$ make install

macOSにてgccのクロスコンパイラのビルドが失敗する

UEFIを試してみるため,MacBookProにgccmingw-w64版のビルドをしようとしたところ,make install-gcc時に,以下のエラーが発生した。

ld: warning: ignoring file ../libiberty/libiberty.a, file was built for archive which is not the architecture being linked (x86_64): ../libiberty/libiberty.a
Undefined symbols for architecture x86_64:
  "__sch_istable", referenced from:
      _main in fixincl.o
      _initialize in fixincl.o
      _process in fixincl.o
      _char_macro_def_fix in fixfixes.o
      _char_macro_use_fix in fixfixes.o
      _format_fix in fixfixes.o
      _wrap_fix in fixfixes.o
      ...
  "__sch_toupper", referenced from:
      _wrap_fix in fixfixes.o
      _gnu_type_fix in fixfixes.o
  "_fdopen_unlocked", referenced from:
      _process in fixincl.o
      _load_file in fixincl.o
      _create_file in fixincl.o
      _proc2_fopen in procopen.o
  "_freopen_unlocked", referenced from:
      _main in fixincl.o
      _initialize in fixincl.o
  "_xcalloc", referenced from:
      _run_compiles in fixincl.o
      _run_shell in server.o
  "_xmalloc", referenced from:
      _process in fixincl.o
      _wrap_fix in fixfixes.o
      _run_shell in server.o
  "_xmalloc_set_program_name", referenced from:
      _initialize in fixincl.o
  "_xrealloc", referenced from:
      _run_shell in server.o
      _load_file_data in fixlib.o
  "_xregcomp", referenced from:
      _compile_re in fixlib.o
      _mn_get_regexps in fixlib.o
  "_xregerror", referenced from:
      _compile_re in fixlib.o
      _mn_get_regexps in fixlib.o
  "_xregexec", referenced from:
      _process in fixincl.o
      _machine_name_test in fixtests.o
      _char_macro_def_fix in fixfixes.o
      _char_macro_use_fix in fixfixes.o
      _format_fix in fixfixes.o
      _machine_name_fix in fixfixes.o
      _wrap_fix in fixfixes.o
      ...
  "_xstrdup", referenced from:
      _run_shell in server.o
  "_xstrerror", referenced from:
      _initialize in fixincl.o
      _process in fixincl.o
      _load_file in fixincl.o
      _create_file in fixincl.o
      _chain_open in procopen.o
      _load_file_data in fixlib.o
ld: symbol(s) not found for architecture x86_64

その後調べると,homebrewでインストールしたbinutilsがエラーを引き起こしているようだった。 なので,思い切ってbrew uninstall binutilsでアンインストールして,以下のコマンドでconfigureしてみた。

../gcc-9.1.0/configure --prefix=**** --target=x86_64-w64-mingw32 --with-gmp=/usr/local/opt/gmp --with-mpfr=/usr/local/opt/mpfr --with-mpc=/usr/local/Cellar/libmpc/1.1.0 AR="/usr/bin/ar" RANLIB="/usr/bin/ranlib"

そしてmake all-gcc -j4make install-gccコンパイル&インストールができた。

Mach-Oの構造メモ

Mach-Oの情報をどこかに書かないと忘れてしまいそうだったため。

Mach-OのリファレンスはAppleの開発者用サイトから削除されてしまったようだが,githubにミラーがある。 Mirror of OS X ABI Mach-O File Format Reference

Mach-Oのリファレンスは誤記がいくつかあるようなので,注意する。

Mach-Oには大きく分けて,Header, Load Commands, Data という三つの領域がある。

Header

Headerは,ELFやPEなどと同じように,マジックナンバーがあり,そのあとにCPUの種類,ファイルの種類などが列挙されている。 以下はHeaderの構造体だ。

/*
 * The 32-bit mach header appears at the very beginning of the object file for
 * 32-bit architectures.
 */
struct mach_header {
    uint32_t   magic;      /* mach magic number identifier */
    cpu_type_t  cputype;    /* cpu specifier */
    cpu_subtype_t   cpusubtype; /* machine specifier */
    uint32_t   filetype;   /* type of file */
    uint32_t   ncmds;      /* number of load commands */
    uint32_t   sizeofcmds; /* the size of all the load commands */
    uint32_t   flags;      /* flags */
};

/* Constant for the magic field of the mach_header (32-bit architectures) */
#define    MH_MAGIC    0xfeedface /* the mach magic number */
#define MH_CIGAM   0xcefaedfe /* NXSwapInt(MH_MAGIC) */

/*
 * The 64-bit mach header appears at the very beginning of object files for
 * 64-bit architectures.
 */
struct mach_header_64 {
    uint32_t   magic;      /* mach magic number identifier */
    cpu_type_t  cputype;    /* cpu specifier */
    cpu_subtype_t   cpusubtype; /* machine specifier */
    uint32_t   filetype;   /* type of file */
    uint32_t   ncmds;      /* number of load commands */
    uint32_t   sizeofcmds; /* the size of all the load commands */
    uint32_t   flags;      /* flags */
    uint32_t   reserved;   /* reserved */
};

mach_headerが32bit,mach_header_64が64bit用だ。 これらのMach-Oの構造体などは,自分の環境(MacBook Pro 2018, macOS Mojave 10.14.5)の場合,/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/mach-o以下のloader.hにあった。

Load Commands

Load CommandsはHeaderのすぐ後ろにある。以下,Load Commandsのことを"LC"とする。 HeaderのncmdsがLCの数,sizeofcmdsがLC全体のサイズである。

Mach-Oでは,各LCの先頭はコマンドの種類とサイズになる。load_command構造体で,それらを読み込むことができる。

struct load_command {
    uint32_t cmd;      /* type of load command */
    uint32_t cmdsize;  /* total size of command in bytes */
};

Segment Commandは,プログラムを読み込むためのコマンドだ。 以下にSegment Commandの構造体を示す。

struct segment_command { /* for 32-bit architectures */
    uint32_t   cmd;        /* LC_SEGMENT */
    uint32_t   cmdsize;    /* includes sizeof section structs */
    char       segname[16];   /* segment name */
    uint32_t   vmaddr;     /* memory address of this segment */
    uint32_t   vmsize;     /* memory size of this segment */
    uint32_t   fileoff;    /* file offset of this segment */
    uint32_t   filesize;   /* amount to map from the file */
    vm_prot_t   maxprot;    /* maximum VM protection */
    vm_prot_t   initprot;   /* initial VM protection */
    uint32_t   nsects;     /* number of sections in segment */
    uint32_t   flags;      /* flags */
};

/*
 * The 64-bit segment load command indicates that a part of this file is to be
 * mapped into a 64-bit task's address space.  If the 64-bit segment has
 * sections then section_64 structures directly follow the 64-bit segment
 * command and their size is reflected in cmdsize.
 */
struct segment_command_64 { /* for 64-bit architectures */
    uint32_t   cmd;        /* LC_SEGMENT_64 */
    uint32_t   cmdsize;    /* includes sizeof section_64 structs */
    char       segname[16];   /* segment name */
    uint64_t   vmaddr;     /* memory address of this segment */
    uint64_t   vmsize;     /* memory size of this segment */
    uint64_t   fileoff;    /* file offset of this segment */
    uint64_t   filesize;   /* amount to map from the file */
    vm_prot_t   maxprot;    /* maximum VM protection */
    vm_prot_t   initprot;   /* initial VM protection */
    uint32_t   nsects;     /* number of sections in segment */
    uint32_t   flags;      /* flags */
};

Segment Commandの後にはSectionが存在する。 Sectionにはプログラムなどのデータの情報が書かれている。 以下にSectionの構造体を示す。

struct section { /* for 32-bit architectures */
    char       sectname[16];  /* name of this section */
    char       segname[16];   /* segment this section goes in */
    uint32_t   addr;       /* memory address of this section */
    uint32_t   size;       /* size in bytes of this section */
    uint32_t   offset;     /* file offset of this section */
    uint32_t   align;      /* section alignment (power of 2) */
    uint32_t   reloff;     /* file offset of relocation entries */
    uint32_t   nreloc;     /* number of relocation entries */
    uint32_t   flags;      /* flags (section type and attributes)*/
    uint32_t   reserved1;  /* reserved (for offset or index) */
    uint32_t   reserved2;  /* reserved (for count or sizeof) */
};

struct section_64 { /* for 64-bit architectures */
    char       sectname[16];  /* name of this section */
    char       segname[16];   /* segment this section goes in */
    uint64_t   addr;       /* memory address of this section */
    uint64_t   size;       /* size in bytes of this section */
    uint32_t   offset;     /* file offset of this section */
    uint32_t   align;      /* section alignment (power of 2) */
    uint32_t   reloff;     /* file offset of relocation entries */
    uint32_t   nreloc;     /* number of relocation entries */
    uint32_t   flags;      /* flags (section type and attributes)*/
    uint32_t   reserved1;  /* reserved (for offset or index) */
    uint32_t   reserved2;  /* reserved (for count or sizeof) */
    uint32_t   reserved3;  /* reserved */
};

Mach-OのSegment Commandの情報を表示するCプログラムを以下に示す。

readmacho.c:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#include <mach-o/loader.h>

static const char *usage = "Usage: readmacho ¥"filename¥"¥n";

void *read(FILE *fp, size_t size) {
    void *data = calloc(1, size);
    fread(data, size, 1, fp);
    return data;
}

void *load(FILE *fp, int offset, size_t size) {
    void *data = calloc(1, size);
    fseek(fp, offset, SEEK_SET);
    fread(data, size, 1, fp);
    return data;
}

void print_sections(FILE *fp, int nsects, int *current) {
    for (int i = 0; i < nsects; i++) {
        size_t sect_size = sizeof(struct section_64);
        struct section_64 *sect = read(fp, sect_size);
        *current += sect_size;
        printf("  section: %s¥n", sect->sectname);
        printf("    offset: %x¥n", sect->offset);
        free(sect);
    }
}

int main(int argc, char **argv) {
    if (argc < 2) {
        fprintf(stderr, "%s¥n", usage);
        return 1;
    }
    char *name = argv[1];
    FILE *fp;
    if ((fp = fopen(name, "rb")) == NULL) {
        fprintf(stderr, "Can't open the file: %s¥n", name);
        return 1;
    }

    uint32_t *magic = load(fp, 0, sizeof(uint32_t));
    if (*magic != MH_MAGIC_64) {
        fprintf(stderr, "%s is not a mach-o file¥n", name);
        free(magic);
        return 1;
    }
    free(magic);
    
    size_t header_size = sizeof(struct mach_header_64);
    struct mach_header_64 *header = load(fp, 0, header_size);
    int current = header_size;
    
    for (int i = 0; i < header->ncmds; i++) {
        size_t lc_size = sizeof(struct load_command);
        struct load_command *lc = load(fp, current, lc_size);

        if (lc->cmd == LC_SEGMENT_64) {
            size_t seg_size = sizeof(struct segment_command_64);
            struct segment_command_64 *seg = load(fp, current, seg_size);
            current += seg_size;
            printf("segment: %s¥n", seg->segname);
            print_sections(fp, seg->nsects, &current);
            free(seg);
        } else {
            current += lc->cmdsize;
        }
        free(lc);
    }

    free(header);
    fclose(fp);
    return 0;
}

試しに自分自身の情報を表示してみる。

./readmacho readmacho

出力は以下のようになる。

segment: __PAGEZERO
segment: __TEXT
  section: __text
    offset: a82
  section: __stubs
    offset: df4
  section: __stub_helper
    offset: e24
  section: __cstring
    offset: e84
  section: __eh_frame
    offset: f08
segment: __DATA
  section: __nl_symbol_ptr
    offset: 1000
  section: __got
    offset: 1010
  section: __la_symbol_ptr
    offset: 1018
  section: __data
    offset: 1058
segment: __LINKEDIT

Section以下のoffsetは,そのSectionのデータが書かれている位置だ。

Data

Load Commandsの後ろにあるData領域は,その名の通りプログラムやシンボルテーブル,リロケーション情報などが書かれている。 試しに以下のCプログラムをコンパイルして,先ほどのreadmachoで情報を表示してみる。

main.c:

#include <stdio.h>

int main(void) {
    printf("hello, world¥n");
    return 0;
}
gcc -c main.c
./readmacho main.o

すると出力は以下のようになる。

segment:
  section: __text
    offset: 1d8
  section: __cstring
    offset: 1ef
  section: __eh_frame
    offset: 200

__textSectionにはプログラムデータ,__cstringSectionには文字列データ,__eh_franeは,調査不足だが,恐らく例外処理に使われる。

何らかのバイナリエディタでmain.oを開き,__cstringのoffset(ここでは1efだが,環境によって異なるだろう)に飛んでみる。 そこには,hello, worldのasciiコードが書かれているはずだ。 データの横に文字列が表示されるタイプのバイナリエディタであれば,そこにはhello, worldと表示されているはずだ。

編集履歴

Mach-Oに関してはまだ調査不足なので,この記事は随時更新となる。

2019/07/01: 最初のバージョンを公開。

2019/07/02: 誤字の修正。

2019/07/06: リファレンスに関する注意書きを追加。