2012年12月1日土曜日

iOSシミュレーターの限界2;メモリアクセスの罠(第4版)

メモリアクセスにだいぶ間違いがあったので、テスト結果に基づいて再修正。

以前に「iOSシミュレーターの限界」という題で、実機とシミュレーターの違いに付いて書いたが、その後の開発で、さらに違いを見つけたので書いておく。

・CPUが違う

「当たり前」とか言われそうだが、意外にこれは重要な違いである。

私は、iOSシミュレーターはARMシミュレーター上で動いている物だと思っていたのだが、
シミュレーター上では86のコードでコンパイルされているようである。
(アセンブラコード見て気づけ!というのはなしの方向で。)

これがどんな違いをもたらすかというと、 「メモリ境界アクセスでの挙動が全く違う」のである。

ARMと86のメモリアクセスを考慮するとき、見るべきは・エンディアン;L->HなのかH->Lなのか
・バイト境界からの16bit以上アクセスは可能か
・ワード境界からの32bitアクセスは可能か
・ワード境界からの64bitアクセスは可能か
である。

多くの場合はObjective-Cが隠してくれるので気にする必要はないが、Cのライブラリを使うとき、特に構造体アクセスや、ワークエリアに可変長データを書き込む際に、要注意である。

エンディアンについては、ARMも86もリトルエンディアン、すなわちL->Hのアクセスなので、シミュレーターも実機も変わりない。

しかし、境界アクセスについては全く異なる。

86ではどのようなアドレスからでも16bit/32bit/64bitアクセスが可能である。
奇数アドレスからでも。

がしかし、ARMには制約がある。
1. バイト境界からのshort以上アクセスは不可
2. ワード境界からのlongアクセスは可能な時と不可の時がある
3. ワード境界からのdouble(long long)アクセスは不可
4. ロングワード境界からのdoubleアクセスは可能

どれも「ワード境界からなら行けるだろう」と思っていたら制約があったのでひっかかった。

わけがわからないのが2番めのlongアクセス。トラップを発生するときとしない時があり、 どの時できるの解明できていない。
したがって、現状では「できない」ものとして処理したほうがよさそう。

可変ワーク内に各種サイズのデータを書き込むようなプログラムの場合、そのアドレスによっては例外が発生してしまう。この場合はたとえば、

short *word=アドレス;
*word=0x0001;

という式の場合は、

short dt=0x0001;
memcpy(word,&dt,sizeof(word));

と書き換える必要がある。

Xcode V4.60以降には、memcpy()を、転送サイズが2バイト、4バイトまたは8バイトの倍数の時、それぞれshort、longまたはlong long単位で転送するようにインライン展開するというバグ、というかいらんお世話がいる。したがって、memcpy()では回避できないので、常にバイトまたはワード単位で転送するような関数を作って置き換える必要がある。

構造体では、メンバーはアクセスに問題ないように配置されるため、メンバー間整合のための予備バイトが挿入されることが有る。
したがって、厳密にメンバーのサイズとその配置に依存しているプログラムでは予想する動作をしない。この場合、

    #pragma pack(push,2)    構造体定義
    #pragma pack(pop)

と書くことで境界整合を制御できる。上記式は16ビット境界に配置する場合で、バイト境界にまでする場合は(push,1)とする。ただし、こうすると当然メモリアクセス処理が冗長になるので、速度低下を起こす。従って、可能なら位置やサイズへの依存をなくすようにプログラムを修正する方が良い。


拙作「X-BASIC for iOS」は言語処理部分はCで記述しており、しかも中間コードコンパイルではワークエリアへのデータ書き込みを可変長で行っているためこの問題に引っかかった。

設計上ワード境界アクセスにはしていて、シミュレーター上では全く問題なく動いていたのだが、実機上に持って行ったとたん例外が発生して動かなくなったので調べたらこうだった。

設計を変更して根本的回避ができないかも考えたが、構造があまりに複雑になりすぎるので、メモリアクセス側に対策して回避した。

ということで、「やっぱり実機での確認は欠かしたらあかん」「C言語に慣れている人ほど落とし穴にはまりやすい」という話。

----------2013/05/15更にテスト

以下のプログラムでテスト。これがまた、予想に反して全て正常動作してしまうという、結果になった。ARMの挙動、もしくはXcodeコンパイラの挙動はまったくもって不可解。

    // シミュレーター上=86上では全て問題ない
    // ARM上では?
    long work[32]; // このワークは正しい境界整合で確保されるはず
    char *bp=(char *)work;
    NSLog(@"work=%p",bp);
    unsigned long l=(unsigned long)bp;
    while (l&0xf) l++; // $~0まで移動
    bp=(char *)l;

    NSLog(@"work.0=%p",bp);
    //
    // .bからの.w
    short *wp=(short *)(bp+1);
    *wp=0x1122;
    NSLog(@"[%p]=$%x",wp,*wp);
    //
    // .wからの.l
    long *lp=(long *)(bp+2);
    *lp=0x11223344;
    NSLog(@"[%p]=$%lx",lp,*lp);
    //
    // .w境界から.llアクセス
    long long *llp=(long long *)lp;
    *llp=0x1122334455667788;
    NSLog(@"[%p]=$%llx",llp,*llp);

    // .l境界から.llアクセス
    llp=(long long *)(bp+4);
    *llp=0x1122334455667788;
    NSLog(@"[%p]=$%llx",llp,*llp);

ARM Linuxでは例外をカーネルで受けてうまく処理しているらしいので、iOSでも「ある程度」やってくれているのかもしれない。不十分だけど。

ここも参照のこと↓
http://jr0bak.homelinux.net/~imai/linux/arm_gcc_badknowhow/arm_gcc_badknowhow-4.html#ss4.1

0 件のコメント:

コメントを投稿