Arm関連でやってきたこと

西永俊文氏(以下、西永):それでは発表を始めます。今回は初めての方も多そうなので、自己紹介をします。名前は西永俊文といいます。

趣味で10年近くArmのボードで遊んでいます。Cortex-M7マイコンでLinuxを動かしたり、当時はJTAGデバッグの方法が公開されていなかった「SynQuacer」というボードで、JTAGデバッグをできるようにしたりしていました。また、Raspberry Piを題材にハードウェアの初期化部分からコードを書いて開発していく講義をセキュリティ・キャンプで行なったりもしました。その他の活動についてはGitHub、Speaker Deck、Twitterを参照してください。

さて、本資料は勉強会を開いてくれたぬるぽへさん、レビューに協力してくれたインターネットの闇(@no_maddo)さん、これまでお世話になったみなさまのおかげで作られています。みなさま、本当にありがとうございました。

今回の発表では、Armが何かを知らない人のために、Armアーキテクチャの名前や命令セットの名前を知ってもらい、今後の学習の役に立ててもらうのが目的となります。本日はこのあと深い発表がいくつもあるはずなので、この発表では、基本的に広く浅くを意識して発表する予定です。

Armプロセッサーは設計と製造企業が分かれている

それでは本題に入っていきましょう。Armというと範囲が広いのですが、プロセッサーとしてのArmは、Arm社の作ったRISCアーキテクチャのプロセッサーになります。

このArm社はどういった会社かというと、イギリスに本社をもつ、半導体IP、これをわかりやすく例えると半導体の設計図になるかと思うのですが、その半導体IPやその利用ライセンスなどを設計・販売している会社です。実はこの会社は、2016年にソフトバンクに買われたのですが、最近またNVIDIAに買われるというニュースが出てきており、今後が気になる状況になっています。

Armのプロセッサーは、省電力や低価格といった特徴があるため、昔から組み込み機器に多く使われてきました。今でもほぼすべての携帯電話・携帯ゲーム機・ルーターなどに採用されており、みなさまの生活を支えています。ここ数年は組み込みだけでなく、Surface Pro Xやまだ出てきていないMacBookなどのデスクトップ機、そしてサーバー、さらにはスーパーコンピューターの方面にも拡大してきています。

このArmは、省電力・低価格・低発熱といったほかに、設計と製造企業が分かれているという特徴があります。みなさんの中には「なぜArmプロセッサーは、x86と違って、BroadcomやQualcomm、NXPなど複数のメーカーから出てきているのか?」という疑問をもった方がいるかもしれません。その答えはこの特徴にあります。

まず、Arm社はプロセッサーやペリフェラルの設計を行ないますが、チップの製造は行なっておらず、その設計や(設計を)利用するためのライセンスなどを売っています。

代わりに、チップメーカーがArm社からコアのIPやペリフェラルを購入し、自社で設計したペリフェラルなどをくっつけて、System on Chip、略してSoCと呼ばれるチップを製造して販売しています。このチップメーカーにはBroadcomやNXPなど複数の会社があるため、さまざまな会社のArmのチップが売られている状況となっています。

実際に使われているSoCの例として、Raspberry Piに搭載されたBCM2835というチップを見ていきましょう。

BCM2835というSoCは、Armの設計したプロセッサーとUARTのペリフェラルに、Broadcomの作ったGPIOやVideoCoreというGPU、そして割り込みコントローラを追加し、Synopsysの作ったUSBコントローラなどを入れて、Broadcomが製造を行なっています。

アーキテクチャとプロセッサーの違い

さて、ここからはArmアーキテクチャとアセンブリについて少し詳しく紹介していきましょう。コンテンツの一覧はこのようになっています。

最初にアーキテクチャとプロセッサーの違いについて紹介します。Armにはx86などと同様にアーキテクチャ名とプロセッサー名があります。まずはアーキテクチャについて見ていきましょう。

アーキテクチャは、MMUの仕様や命令セットの仕様など、プロセッサーの基礎設計の部分を決めるものとなっています。そのため、例えば同じアーキテクチャのプロセッサーであれば、基本的には同じ命令セットが利用できるというようになっています。

アーキテクチャ名は「ARMv」のあとに数字が入ります。最新世代は「ARMv8」となっているのですが、最近は少し刻んで「ARMv8.1」などの小数点部分のアップデートがあったりもします。

アーキテクチャの後ろには、「A」「R」「M」の一文字が追加でつくようになっています。

このアーキテクチャの後ろにつく「A」「R」「M」はプロセッサーファミリーを示しています。 「A」はスマートフォンやサーバーなど高性能なプロセッサーファミリーとなっており、MMUやTrustZoneなど多くの機能を備えています。

「R」はリアルタイムシステム向けのファミリーとなっているのですが、私自身はRのチップを実は見たことも触ったこともないという状況なので、深くは紹介できません。アーキテクチャ・マニュアルを読むかぎりでは、MMUの代わりにMPUが搭載されているなどの細かな差異があるようです。

「M」はワンチップマイコン向けのファミリーとなっており、使える命令セットがThumbまたはThumb2命令のみなど、さまざまな制限のあるコアとなっています。

次にプロセッサーについて紹介しましょう。プロセッサーとはアーキテクチャの仕様をもとに作られています。

現行ではCortexシリーズが出ていて、A・R・Mなどに数字がつく表記です。big.LITTLEというものがあるため、例外はあるのですが、基本的にはこの一番後ろについている数字が大きいほうが世代が新しく、性能がよく、新しい命令が使えたりするようになっています。

ここで小ネタになりますが、big.LITTLEというArmの省電力化のためのソリューションがあります。これは高性能ですが電池を食うbigコアと、低性能だけれども省電力なLITTLEコアをセットに搭載して、負荷に応じて使うコアを切り替えることによって消費電力を抑えます。セットの例としては、Cortex-A15のbigコアとCortex-A7のLITTLEコア、Cortex-A73のbigコアとCortex-A53のLITTLEコアのセットなどがあります。

Armアーキテクチャだいたいの歴史

話を戻しまして、アーキテクチャとプロセッサーの差がわかったところで、Armのアーキテクチャのだいたいの歴史を眺めてみましょう。

まず、私が初めて触ったArmデバイスはゲームボーイアドバンスになると思います。このころはARMv3〜ARMv4アーキテクチャのARM7やARM7TDMIと呼ばれるプロセッサーが使われていました。

次のARMv5の世代では、ARM10のプロセッサーが搭載されたゲーム機や、Intelの作った「XScale」というArmのプロセッサーが搭載された、PDAというスマートフォンの前身のようなものが世に出ていました。

iPhoneや初代Raspberry PiではARMv6アーキテクチャのARM11というプロセッサーが使われていました。このあとのARMv7アーキテクチャからCortexシリーズが始まり、多くのスマートフォンやゲーム機、そしてサーバーやスパコンなどで使われるようになっています。

実行モードについて

次にArmの実行モードについて説明していきましょう。ARMv7の世代までは32bitのモードしかなかったのですが、ARMv8で64bit実行の「AArch64」モードが増えました。そしてこれまでの32bitモードは「AArch32」モードという名前がついて、それらを切り替えながら利用できるようになりました。

この各モードの呼ばれ方は、OSや資料、機能や歴史的経緯などによっていくつか種類があります。例えば32bitモードは、「oabi」という今ではもうサポートされていないABIを使った「arm」という呼ばれ方、「eabi」という新しいABIが使われている「armel」、ハードウェア浮動小数点演算器を搭載したものに対してつけられる「armhf」、単に32bitの実行モードを表す「A32」という呼ばれ方があります。

(スライドを示し)基本的には上3つがLinuxだとかのディストリビューションで使われていて、下の「AArch32」はArmの資料やAppleなどでよく使われます。

64bitモードは、同じくLinuxディストリビューションでよく使われる「arm64」という呼ばれ方と、クロスコンパイラ、Armの資料、あとAppleとかでよく使われる「AArch64」、略して「A64」などの2種類があります。

レジスタの紹介

Armアセンブリを紹介するためにはレジスタの紹介が必要となるので、ここで紹介します。実行モードのA32とA64でレジスタも別物となっているので、それらは分けて紹介いたします。

まずA32モードのレジスタを紹介します。A32モードでは主に16個のレジスタを扱います。汎用レジスタは32bitのものが13本あり、そのほかにスタックポインタやリンクレジスタ、プログラムカウンタの3つのレジスタがあります。

リンクレジスタはArmの特徴的なレジスタになっていて、関数ジャンプのあとに戻ってくるためのアドレスを格納しておくレジスタとなっています。Branch withLink(BL)命令を使ってあらかじめジャンプを行なっておくことで、深さ1までであれば、このレジスタのアドレスを参照して、また元の位置に戻ってこれるようになっています。

次にA64モードのレジスタを紹介します。A32モードのレジスタとの違いとしては、ほとんどのレジスタが64bit化されている点です。

汎用レジスタの数も13本から31本まで増えました。加えて、0を読み出すことができるゼロレジスタが増えたので、0を作るために即値のロードなどを行なう必要がなくなり、少し便利になっています。

そのほか、スタックポインタやプログラムカウンタがシステムレジスタになって気軽にアクセスできなくなったといったこと以外は、だいたいA32モードのレジスタとほぼ同じなので、説明を省略します。

アセンブリの話

さて、ここからは軽くアセンブリの話をしていきたいと思います。現在Armアセンブリの命令セットは、A32・A64・Thumbという大きく分けて3つの種類があります。順番に説明していきましょう。

A32アセンブリは32bitの固定長命令セットになります。あまりほかに見ないおもしろい機能としては、条件付き命令実行とバレルシフタというものがあります。

この条件付き命令実行は、任意の命令を条件付き実行にできる機能です。例えばAND命令のあとに「EQ」という条件をつけると、直前の演算結果が0のときにAND命令を実行できるようになります。何もつけない場合は、コンパイラが無条件実行の「AL」というものをつけて実行します。

オペコード的には、頭の4bitにこの条件付き実行のためのフィールドがあり、ほとんどの命令は無条件実行で0x0Eが頭に来るようになっているので、Armのバイナリをバイナリエディタで開くと、0x0Eが一定間隔に並ぶ様子が観測されます。

実際にコードを書いてアセンブリを行ない、逆アセンブルした結果を見比べてみたものがこちらです。左側は自分の手で書いたアセンブリ、右側が逆アセンブルした結果です。こちらを見てみると、andeqがきちんとアセンブルを通っていることや、andalとandの命令が同じオペコードになっている様子が確認できます。

バレルシフタについて

次にバレルシフタについて紹介しましょう。バレルシフタは命令内でレジスタの値をシフトできる機能になります。例えばレジスタの値を1ビットシフトしてから、ほかのレジスタに入れるということをしたり、A32命令では基本的に16bitを超える即値はメモリからロードする必要があるのですが、8bitをシフトして得られる即値であれば、16bitを超える即値をmov命令でレジスタに入れることができます。

これを使うメリットとしては、命令数の削減ができたり、でかい値をそのままロードできたりします(※発表者注:発表時はA64ではできないといいましたが、誤りでした。バレルシフタはA64でも利用可能です)。

実際のコードを見てみましょう。通常、8行目のように16bitを超える即値をロードするコードは相対アドレス指定でメモリからロードされます。しかし、6行目と7行目のように8bit以内の値からシフトして得られる即値は、バレルシフタの機能のおかげで1命令で読み込めています。

メモリからのロードは時間がかかるので、コードサイズの削減でなく、実行時間の節約も可能かと思います。

A64アセンブリ

ここからはA64アセンブリを紹介していこうと思います。A64アセンブリもA32と同じく32bit固定長の命令セットとなります。いまのところA32のような特徴的な機能があまりなく、かなり親切で、素直で、書きやすい命令セットです。

個人的な感想としては、レジスタの数がほぼ倍に増えたのに、命令長を32bitに抑えることができていて、しかも一部の命令が開発者にわかりやすいようになっている点がすばらしいと思っています。

実際に階乗を計算するC言語のコードをA32とA64アセンブリ向けにコンパイルして逆アセンブルした結果を見比べてみましょう。左がA32のコード、右がA64のコードになります。

差分としては、A32ではレジスタを一斉にスタックに積むLDMやSTM命令というのがあったのですが、A64のほうではそれがなくなり、STPやLDP命令で2個ずつしかレジスタをスタックに積むことができなくなりました。

そのほか、A32ではbx命令で関数から戻っていたところが、A64のほうではret命令でできるようになるなど、開発者にわかりやすくなっている点が見受けられると思います。

Thumbアセンブリ

最後に紹介するのはThumbアセンブリです。これらは基本的に命令長を縮めて省メモリ化や高効率化を狙った命令です。

Thumb命令には初代ThumbとThumb2の2つの命令セットがあります。初代Thumb命令セットは、ARM7プロセッサーのあたりで使えるようになった、ほぼ16bit固定長の命令セットです。現在では最もコンパクトなマイコン向けプロセッサーのCortex-M0、Cortex-M0+で利用されています。

汎用レジスタの数はA32と変わらないのですが、(Thumb命令セットは)命令長の制限でほとんどの命令がレジスタをR0からR7までの8つしか扱えないなど、さまざまな制限があり、書くのが非常に大変な命令です。

Thumb2命令のほうは、命令長が16bit・32bitの混合となった代わりに、使える命令が増えたりレジスタの制限がなくなるなど、大変使い勝手が向上した命令セットです。Cortex-M3以上のプロセッサーで利用されており、またCortex-Aシリーズの世代でも利用が可能です。

実際にThumbとThumb2で階乗のC言語のコードをビルドして逆アセンブルしてみた結果が、こちらです。左がThumbのほう、右がThumb2のコードです。

今回はコード自体がとても短いので差がわかりづらいのですが、左のThumbのコードが16bit固定長になっていることや、右のThumb2のほうが混合命令長になっていることがわかると思います。

あとは、ちょっと地味なのですが、Thumb2のほうではcbnzという0と比較する命令が使えているなどの差が見られて、コード自体はちょっと行数が短くなっている様子が見えるかなと思います。

まとめ

最後にまとめに入っていきましょう。

Armというのは、基本的にはArm社の作ったRISCアーキテクチャのプロセッサーです。主に組み込みで利用されてきましたが、最近はサーバーやスパコンの分野にも進出してきています。Armのアーキテクチャ名は「ARMv〇〇」、プロセッサー名は「Cortex-〇〇」となっています。

最近のArmは64bitモードがあり、この64bitモードで使えるA64命令セットはとても素直で使いやすいので、これからみなさんどんどん使っていっていただければいいんじゃないかなと思います。

というところで、今回の発表を終わらせていただきます。引き続き本勉強会を楽しんでください。ありがとうございました。