Linux
GCC
gas
アセンブラ
カーネル

インラインアセンブラの読み方

はじめに

本稿では、Linuxカーネルの実装1をサンプルとして、簡単なインラインアセンブラ2の読み方を説明します。

前提知識

  • C言語の基本文法
  • Intel CPUアーキテクチャの基礎知識

ゴール

Linuxカーネルのインラインアセンブラを雰囲気で読めるようになることです。

アセンブラとは

C言語は高級言語で、アセンブラは低級言語と呼ばれますが、アセンブラもプログラミング言語の一種です。プログラムはコンピュータ上で動かすことができますが、コンピュータ(CPU)が解釈できる命令は機械語3だけです。
機械語を直接記述するのは大変なので、CPUの命令と一対一に対応した言語が作られました。それがアセンブラです。厳密にはその言語のことを「アセンブリ言語」と呼び、アセンブリ言語を機械語に変換するソフトウェアのことを「アセンブラ」と呼びます。
一般的に、C言語のプログラムはコンパイラにより、アセンブリ言語に変換した後、機械語に変換することで、実行プログラムを生成します。

サンプルコード

Linuxカーネル4.16(2018年4月リリース)のx86およびx64向けのmemcpy関数を例として挙げます。
Linuxカーネルはアプリケーションプログラムではないため、glibcの機能が利用できず、カーネル内で独自にmemcpy関数などの処理が実装されています。

以下にソースコードを示します。パスは「arch/x86/boot/compressed/string.c」になります。

x86(32bit)向け

static void *__memcpy(void *dest, const void *src, size_t n)
{
    int d0, d1, d2;
    asm volatile(
        "rep ; movsl\n\t"
        "movl %4,%%ecx\n\t"
        "rep ; movsb\n\t"
        : "=&c" (d0), "=&D" (d1), "=&S" (d2)
        : "0" (n >> 2), "g" (n & 3), "1" (dest), "2" (src)
        : "memory");

    return dest;
}

x64(64bit)向け

static void *__memcpy(void *dest, const void *src, size_t n)
{
    long d0, d1, d2;
    asm volatile(
        "rep ; movsq\n\t"
        "movq %4,%%rcx\n\t"
        "rep ; movsb\n\t"
        : "=&c" (d0), "=&D" (d1), "=&S" (d2)
        : "0" (n >> 3), "g" (n & 7), "1" (dest), "2" (src)
        : "memory");

    return dest;
}

サンプルコードの意味

x86のコードの処理は以下の通りです。

  1. 4バイトずつsrcからdestにコピーする。
  2. コピー総サイズが4の倍数ではない場合、端数をsrcからdestにコピーする。

x64のコードの処理は以下の通りです。

  1. 8バイトずつsrcからdestにコピーする。
  2. コピー総サイズが8の倍数ではない場合、端数をsrcからdestにコピーする。

x86とx64で一度にコピーするサイズが異なるのは、それぞれのCPUが一度に扱えるデータサイズに合わせているからであり、そうすることで高速に処理することができるのです。

x86のアセンブラコードをC言語で記述すると、例えば以下のようになります。

static void *__memcpy_C(void *dest, void *src, size_t n)
{
    int i, len, rem;
    int *pd, *ps;
    char *ppd, *pps;

    len = n / 4;
    rem = n % 4;
    pd = dest;
    ps = src;
    for (i = 0 ; i < len ; i++) {
        *pd++ = *ps++;
    }

    ppd = (char *)pd;
    pps = (char *)ps;
    for (i = 0 ; i < rem ; i++) {
        *ppd++ = *pps++;
    }

    return dest;
}

インラインアセンブラを読む

gcc4ではインラインアセンブラは以下の書式で記述します。

asm volatile(
    AssemblerTemplate
    : OutputOperands
    : InputOperands
    : Clobbers)

"volatile"はコンパイラの最適化を抑止するための指定で、そもそも人の手でアセンブリ言語を使ってまで処理を高速化するのが目的なので、コンパイラによる最適化はむしろされてしまっては困るからです。
"AssemblerTemplate"はアセンブラコードに該当する部分で、実際にCPU上で動くところになります。
"OutputOperands"は書き換えが行われるCPUのレジスタ群を記述するところです。
"InputOperands"は書き換えが行われないCPUのレジスタ群を記述するところです。
"Clobbers"はコンパイラに待避が必要なレジスタ群を指示するところです。特別な指示として"cc"と"memory"があります。

x86のコードで言えば、以下の部分がアセンブリ言語の実体となります。"rep"、"movsl"、"movl"、"movsb"がCPUの命令に対応する指令であり、このそれぞれがアセンブリ言語です。ニーモニック(mnemonic)とも呼びます。一行で一命令(一ニーモニック)が基本ですが、セミコロン(;)で複数の命令を記述することもできます。

        "rep ; movsl\n\t"
        "movl %4,%%ecx\n\t"
        "rep ; movsb\n\t"

上記で使われているアセンブリ言語の意味を以下に示します。

命令 単語の意味 説明
rep repeat ECXが0になるまで後続の命令を繰り返す
movsl move string long [ESI]から[EDI]に4バイトコピーする
movl OP1,OP2 move long OP1からOP2に4バイトコピーする
movsb move string byte [ESI]から[EDI]に1バイトコピーする

"movl"の行に「%4」という指定がありますが、これは引数を意味します。"OutputOperands"と"InputOperands"に指定した引数が順番に%0からアサインされます。「%4」はコピー総サイズが4の倍数ではない場合の端数を示すことが分かります。

引数 命令
%0 "=&c" (d0)
%1 "=&D" (d1)
%2 "=&S" (d2)
%3 "0" (n >> 2)
%4 "g" (n & 3)
%5 "1" (dest)
%6 "2" (src)

"OutputOperands"の引数を見ていきます。「"=&c" (d0)」というのは、ローカル変数d0をECXレジスタに割り当てるという意味です。"=&"の直後にある文字はConstraints(制約文字)といいます。

制約文字 説明
c ECXレジスタ
D EDIレジスタ
S ESIレジスタ

"InputOperands"の引数を見ていきます。
「"0" (n >> 2)」は、nを4で割った値を%0と同じレジスタに割り当てるという意味です。つまり、ECXレジスタに「nを4で割った値」が代入されます。
「"g" (n & 3)」は、nを4で割った余り(剰余)を任意のレジスタに割り当てるという意味です。
「"1" (dest)」は、destを%1と同じレジスタに割り当てるという意味です。つまり、EDIレジスタに「dest」が代入されます。
「"2" (src)」は、srcを%2と同じレジスタに割り当てるという意味です。つまり、ESIレジスタに「src」が代入されます。

"Clobbers"の引数では"memory"が指定されています。これはコンパイラにメモリ領域を変更するということを指示しています。

アセンブラコードの確認

実際にコンパイラがどのようなアセンブラコードを生成するのかを見てみましょう。gccコマンドに"-S"オプションをつけることで、アセンブラコード(.sという拡張子のファイル)を生成することができます。

x86の場合
# cc -m32 -S asm32.c

x64の場合
# cc -m64 -S asm64.c

以下、x86のアセンブラコードから抜粋します。

    ↓スタックから変数nをEAXレジスタに代入する
    subl    $16, %esp
    movl    16(%ebp), %eax
    ↓EAXレジスタの値を2bit右シフトする(4で割る)
    shrl    $2, %eax
    ↓EAXレジスタの値をECXレジスタに代入する
    movl    %eax, %ecx

    ↓スタックから変数nをEAXレジスタに代入する
    movl    16(%ebp), %eax
    ↓EAXレジスタの値を3(11b)でANDする(4の剰余)
    andl    $3, %eax
    ↓EAXレジスタの値をEBXレジスタに代入する
    movl    %eax, %ebx

    ↓スタックから変数destをEDXレジスタに代入する
    movl    8(%ebp), %edx
    ↓スタックから変数srcをEAXレジスタに代入する
    movl    12(%ebp), %eax
    ↓EDXレジスタの値をEDIレジスタに代入する
    movl    %edx, %edi
    ↓EAXレジスタの値をESIレジスタに代入する
    movl    %eax, %esi
/APP
 # 10 "asm32.c" 1
    rep ; movsl
    movl %ebx,%ecx
    rep ; movsb
 # 0 "" 2
/NO_APP

参考文献


  1. インプリ(implementation)、ソースコード、プログラムなどと呼ぶこともある。 

  2. C言語のソースコード中に、部分的にアセンブラのコードを記述すること。 

  3. マシン語とも呼ぶ。見た目は数字の羅列。 

  4. Linuxカーネルのビルドに使われるコンパイラ。