LoginSignup
14
15

More than 5 years have passed since last update.

64bit環境のobjc_msgSendについて調べた

Last updated at Posted at 2016-02-21

2015年からiOSアプリは64bit対応が必須になった。それに伴い、 objc_msgSend にキャストが必要になったのでそれについて書く。(途中まで書いてほったらかしになっていた)

objc_msgSend はオブジェクトのメソッドを呼び出す関数である。
ダイナミックObjective-C メッセージ送信(1) - objc_msgSendの実装

以前のプロトタイプ宣言
id objc_msgSend(id self, SEL op, ...);
今のプロトタイプ宣言
void objc_msgSend(void);

以前は可変長引数を取る関数として宣言されていた。任意の関数(引数が1個だったり2個だったり)を呼べるようにするため、便宜上可変長引数を取る関数として記述するというのはC言語ではよくあるテクニックである。これによりセレクタ以降の引数が何個であってもキャストなしで呼び出すことができた。
今はvoidになっていて、キャストしないと呼び出せないようになっている。
これは(iOSの)ARMの64bit環境にて、可変長引数を取る関数と、そうでない普通の関数の呼び出し規約が異なることに起因している。

Appleが定めた呼出し規約 ARM64 Function Calling Conventions は、ARMによる呼出し規約 Procedure Call Standard for the ARM 64-bit Architecture (AArch64) を何箇所か改変していて、可変長引数をスタックに置くようになっている。

呼び出し規約(calling conventions)とは、関数を呼び出す側(caller)と呼び出される関数(callee)について、呼び出すときの引数の渡し方や、戻り値の受け取り方など、レジスタやスタックの用途やレイアウトについて規定したものである。呼び出し規約はCPUや言語やコンパイラなど、プラットフォーム毎に規定される。ここではiOSのC言語の呼び出し規約について述べるが、下記のように、OS(iOS)と言語(C)が同じでもCPUアーキテクチャによって呼出し規約は異なる。

例として、9つの引数を受け取る hoge 、1つの固定引数と可変長の引数を受け取る fuga 、2つの固定引数と可変長の引数を受け取る piyo を、シミュレータと実機で比較してみる。

hoge
int64_t hoge(int64_t a, int64_t b, int64_t c, int64_t d, int64_t e, int64_t f, int64_t g, int64_t h, int64_t i) {
    return a;
}

int64_t fuga(int64_t a, ...) {
    return a;
}

int64_t piyo(int64_t a, int64_t b, ...) {
    return a;
}

void test() {
    hoge(1, 2, 3, 4, 5, 6, 7, 8, 9);
    fuga(1, 2, 3, 4, 5, 6, 7, 8, 9);
    piyo(1, 2, 3, 4, 5, 6, 7, 8, 9);
}

64bitのシミュレータ(x86-64)の場合、上記三つの関数呼び出しは同じレイアウトになる。
(以下はアセンブリ出力)

    0x1023e7669 <+121>:  movl   $0x1, %r8d
    0x1023e766f <+127>:  movl   %r8d, %edi
    0x1023e7672 <+130>:  movl   $0x2, %r8d
    0x1023e7678 <+136>:  movl   %r8d, %esi
    0x1023e767b <+139>:  movl   $0x3, %r8d
    0x1023e7681 <+145>:  movl   %r8d, %edx
    0x1023e7684 <+148>:  movl   $0x4, %r8d
    0x1023e768a <+154>:  movl   %r8d, %ecx
    0x1023e768d <+157>:  movl   $0x5, %r8d
    0x1023e7693 <+163>:  movl   $0x6, %r9d
    0x1023e7699 <+169>:  movl   $0x7, %r10d
    0x1023e769f <+175>:  movl   %r10d, %eax
    0x1023e76a2 <+178>:  movl   $0x8, %r10d
    0x1023e76a8 <+184>:  movl   %r10d, %r11d
    0x1023e76ab <+187>:  movl   $0x9, %r10d
    0x1023e76b1 <+193>:  movl   %r10d, %ebx
    0x1023e76b4 <+196>:  movq   $0x7, (%rsp)
    0x1023e76bc <+204>:  movq   $0x8, 0x8(%rsp)
    0x1023e76c5 <+213>:  movq   $0x9, 0x10(%rsp)
    0x1023e76ce <+222>:  movq   %rbx, -0x70(%rbp)
    0x1023e76d2 <+226>:  movq   %r11, -0x78(%rbp)
    0x1023e76d6 <+230>:  movq   %rax, -0x80(%rbp)
    0x1023e76da <+234>:  callq  0x1023e7540  ; hoge

debug buildなので無駄なコードがあってわかりづらいが、第一引数から順番に、rdi、rsi、rdx、rcx、r8、r9、スタック、スタック、スタックに設定している。
一方、64bitの実機の場合は、レイアウトがすべて異なる。

    0x1000702ac <+136>:  orr    x0, xzr, #0x1
    0x1000702b0 <+140>:  orr    x1, xzr, #0x2
    0x1000702b4 <+144>:  orr    x2, xzr, #0x3
    0x1000702b8 <+148>:  orr    x3, xzr, #0x4
    0x1000702bc <+152>:  movz   x4, #0x5
    0x1000702c0 <+156>:  orr    x5, xzr, #0x6
    0x1000702c4 <+160>:  orr    x6, xzr, #0x7
    0x1000702c8 <+164>:  orr    x7, xzr, #0x8
    0x1000702cc <+168>:  movz   x8, #0x9
    0x1000702d0 <+172>:  str    x8, [sp]
    0x1000702d4 <+176>:  bl     0x100070170  ; hoge
    0x100070340 <+284>:  mov    x8, sp
    0x100070344 <+288>:  movz   w11, #0x9
    0x100070348 <+292>:  mov    x9, x11
    0x10007034c <+296>:  str    x9, [x8, #56]
    0x100070350 <+300>:  orr    w11, wzr, #0x8
    0x100070354 <+304>:  mov    x9, x11
    0x100070358 <+308>:  str    x9, [x8, #48]
    0x10007035c <+312>:  orr    w11, wzr, #0x7
    0x100070360 <+316>:  mov    x9, x11
    0x100070364 <+320>:  str    x9, [x8, #40]
    0x100070368 <+324>:  orr    w11, wzr, #0x6
    0x10007036c <+328>:  mov    x9, x11
    0x100070370 <+332>:  str    x9, [x8, #32]
    0x100070374 <+336>:  movz   w11, #0x5
    0x100070378 <+340>:  mov    x9, x11
    0x10007037c <+344>:  str    x9, [x8, #24]
    0x100070380 <+348>:  orr    w11, wzr, #0x4
    0x100070384 <+352>:  mov    x9, x11
    0x100070388 <+356>:  str    x9, [x8, #16]
    0x10007038c <+360>:  orr    w11, wzr, #0x3
    0x100070390 <+364>:  mov    x9, x11
    0x100070394 <+368>:  str    x9, [x8, #8]
    0x100070398 <+372>:  orr    w11, wzr, #0x2
    0x10007039c <+376>:  mov    x9, x11
    0x1000703a0 <+380>:  str    x9, [x8]
    0x1000703a4 <+384>:  orr    w11, wzr, #0x1
    0x1000703a8 <+388>:  mov    x1, x11
    0x1000703ac <+392>:  stur   x0, [x29, #-96]
    0x1000703b0 <+396>:  mov    x0, x1
    0x1000703b4 <+400>:  bl     0x1000701b0  ; fuga
    0x1000704a8 <+644>:  mov    x8, sp
    0x1000704ac <+648>:  movz   w11, #0x9
    0x1000704b0 <+652>:  mov    x9, x11
    0x1000704b4 <+656>:  str    x9, [x8, #48]
    0x1000704b8 <+660>:  orr    w11, wzr, #0x8
    0x1000704bc <+664>:  mov    x9, x11
    0x1000704c0 <+668>:  str    x9, [x8, #40]
    0x1000704c4 <+672>:  orr    w11, wzr, #0x7
    0x1000704c8 <+676>:  mov    x9, x11
    0x1000704cc <+680>:  str    x9, [x8, #32]
    0x1000704d0 <+684>:  orr    w11, wzr, #0x6
    0x1000704d4 <+688>:  mov    x9, x11
    0x1000704d8 <+692>:  str    x9, [x8, #24]
    0x1000704dc <+696>:  movz   w11, #0x5
    0x1000704e0 <+700>:  mov    x9, x11
    0x1000704e4 <+704>:  str    x9, [x8, #16]
    0x1000704e8 <+708>:  orr    w11, wzr, #0x4
    0x1000704ec <+712>:  mov    x9, x11
    0x1000704f0 <+716>:  str    x9, [x8, #8]
    0x1000704f4 <+720>:  orr    w11, wzr, #0x3
    0x1000704f8 <+724>:  mov    x9, x11
    0x1000704fc <+728>:  str    x9, [x8]
    0x100070500 <+732>:  orr    w11, wzr, #0x1
    0x100070504 <+736>:  mov    x1, x11
    0x100070508 <+740>:  orr    w11, wzr, #0x2
    0x10007050c <+744>:  mov    x2, x11
    0x100070510 <+748>:  str    x0, [sp, #104]
    0x100070514 <+752>:  mov    x0, x1
    0x100070518 <+756>:  mov    x1, x2
    0x10007051c <+760>:  bl     0x1000701c0  ; piyo

hoge は最初の8個の引数がレジスタで渡され、9個目だけがスタックで渡される。 fuga は最初の引数だけ、 piyo は先頭の二つの引数だけがレジスタで渡される。

以前のプロトタイプ宣言
id objc_msgSend(id self, SEL op, ...);

objc_msgSend の古い宣言は piyo と同じレイアウトである。
iOSのARM64環境で古い宣言をそのまま使うと、呼び出す側は、先頭の二つ(idとセレクタ)だけをレジスタで渡す。
なので呼び出される方が(可変長引数ではない) hoge のような関数だと、3つ目以降の引数(Objective-Cでは一つ目の引数)を受け取ることができないのである。
ちなみに他の環境(ARMの32bit、x86の32/64bit)では、レイアウトが同じなのでこの問題は起きない。(渡すサイズによってはもしかすると同じでないケースがあるかも)

なぜこうしたのか理由はよくわからないが、可変長引数が常にスタックにあるほうがコンパイラの実装がシンプルになるのかもしれない。

この問題の対処のため、Xcodeのビルド設定のPreprocessingにEnable Strict Checking of objc_msgSend Callsが追加された。Yes(新規プロジェクトのデフォルト)だとキャストしないとコンパイルエラーになるので新しいプロジェクトでは問題はあまり起きていないと思われる。

  • 2017/08 補足

WWDCにて、呼び出し規約が異なる理由をAppleのエンジニアに確認したところ、「Appleは古いARMの呼び出し規約を使用しており、ARMが途中で変更したが、すでにコード資産があったため、Appleは変更しないことにした」という回答だった。
もしかするとLLVMに移行する前の呼び出し規約をそのまま使っているのかもしれない。

14
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
15