[Linux] arm と arm64

64bit ARMプロセッサ

携帯機器のほぼ標準となっている ARM プロセッサは、32-bit の v7 アーキテクチャから 64-bit の v8 アーキテクチャに移行しつつあります。とはいえまだソフトウェアは v7 アーキテクチャ向けに書かれたものばかりですので、いきなり移行することはできません。そのため、32-bit と 64-bit を保護レベルの切り替え時にモード切替することができるようになっていて、互換性を保つように設計されています。x86 プロセッサも全く新しい IA-64 アーキテクチャを発表したものの、後で AMD が amd64 アーキテクチャで 32-bit と互換性を保つ技術を導入し、Intel が同調した経緯があります。

boot 時は 64-bit(AArch64) で起動した場合、EL1(OSレベル)やEL0(userレベル) に切り替える際に 32-bit(AArch32) モードを選択することができます。

Linux の互換性

Linux では ARMv7 までのアーキテクチャを「arm」、ARMv8 アーキテクチャを「arm64」と別々のアーキテクチャで扱っています。一方 gcc ではARMの呼び名に合わせてそれぞれ「AArch32」「AArch64」と区別しているので注意が必要です。

arm64 kernel  では、CONFIG_COMPAT を有効にすることで  userland で AArch32 ELF バイナリを起動したときに AArch32 モードに切り替えて実行できるようになっています。arch/arm64/Kconfig には以下のように説明書きがあり、モードの切り替えだけでなく syscall や VFP サポートなどの基本的な機能もアーキテクチャの差異をを超えてサポートされています。

config COMPAT
	bool "Kernel support for 32-bit EL0"
	depends on ARM64_4K_PAGES || EXPERT
	select COMPAT_BINFMT_ELF
	select HAVE_UID16
	select OLD_SIGSUSPEND3
	select COMPAT_OLD_SIGACTION
	help
	  This option enables support for a 32-bit EL0 running under a 64-bit
	  kernel at EL1. AArch32-specific components such as system calls,
	  the user helper functions, VFP support and the ptrace interface are
	  handled appropriately by the kernel.

	  If you use a page size other than 4KB (i.e, 16KB or 64KB), please be aware
	  that you will only be able to execute AArch32 binaries that were compiled
	  with page size aligned segments.

	  If you want to execute 32-bit userspace applications, say Y.

AArch32 ELF バイナリを arm64 kernel で動かすには、次に示すようにいろいろ準備や注意が必要になってきます。

shared library の対応

単純に AArch32 ELF だけの root filesystem であれば何もする必要はないのですが、AArch64 ELF と共存する環境を作る場合は両方の shared library を用意する必要があります。debian などの distro であれば設定で解決できますが、ここでは構成を知るためにとりあえず手動で作る場合を考えてみます。

まず AArch64 ELF の sysroot (sysroot64/) を基準に sysroot をコピーし、AArch32 ELF の sysroot (sysroot32/) からライブラリをコピーします。

$ cp -pr sysroot64         sysroot
$ cp -pr sysroot32/lib     sysroot/lib32
$ cp -pr sysroot32/usr/lib sysroot/usr/lib32

kernel からプロセスとして ELF バイナリを起動する際に、依存関係のあるライブラリは共有ライブラリをリンクします。
その際に動的リンカ(ld.so)が呼び出されますが、このリンカを ELF のアーキテクチャごとに用意しておくことで、マルチアーキテクチャに対応できます。上記の例の場合、AArch32 ELF のローダを /lib に置きます。/lib にはもちろん AArch64 ELF ローダもあります。

$ cp sysroot32/lib/ld-linux-armhf.so.3 sysroot/lib
$ ls -1 sysroot/lib/ld*
ld-2.23.so
ld-linux-armhf.so.3
ld-linux-aarch64.so.1

ターゲット環境ではライブラリの検索パスを指定します。AArch64 ELF と AArch32 ELF の両方のパスを指定しておけば、実行時に自動的に判別して検索します。

# export LD_LIBRARY_PATH=/lib:/usr/lib:/lib32:/usr/lib32

これで arm64 kernel で AArch32 ELF バイナリを起動することができます。

 

device driver の対応

ioctl() は通常 struct file_operations の .unlocked_ioctl メンバに登録した関数を呼び出しますが、異なるアーキテクチャの場合は .compat_ioctl に登録した関数が呼ばれます。この時、引数が 32bit 定義でやってくるので .compat_ioctl 登録関数にて引数や戻り値を変換する必要があります。struct file_operations は linux/fs.h に定義されています。

struct file_operations {
        :
        long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
        long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
        :
};

また、ARM Linux が ILP32 (int/long/pointerが32bit) である一方、arm64 Linux は LP64 (long/pointerが64bit、intが32bit) を採用しています。そのため、sizeof で得られる型のサイズがアーキテクチャによって変化します。これを引数に使うと意味が変わってきます。
特に 第二引数の cmd は マクロ _IOC() を使って定義しますが、定義内に sizeof() を使うためそのままでは 値が変わってしまうことになります。

#define _IOC(dir,type,nr,size) \
        (((dir)  << _IOC_DIRSHIFT) | \
         ((type) << _IOC_TYPESHIFT) | \
         ((nr)   << _IOC_NRSHIFT) | \
         ((size) << _IOC_SIZESHIFT))

#define _IOC_TYPECHECK(t) (sizeof(t))

例えば以下の定義を kernel/user で共有した場合、

#define HOGE_IOC_TEST    _IOC(_IOC_WRITE, 'H', 0x00, long) 

アプリ側からみた HOGE_IOC_TEST は 0x40044800 で ドライバ側から見た HOGE_IOC_TEST は 0x40084800 になり、cmd を正しく判別できなくなるため、型サイズが変わる引数を避けるか .compat_ioctl で cmd をマスクして判別する必要があります。

また、第三引数を構造体へのポインタとする場合にも注意が必要になります。構造体のメンバのメモリ位置やサイズもアプリとドライバで変わってしまう上、packed struct 属性を付けたりすると変換が複雑になってしまうため、避ける必要があります。


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です