虎の穴開発室ブログ

虎の穴ラボ所属のエンジニアが書く技術ブログです

MENU

cgoを使った、Go言語とアセンブリ言語での値のやりとり

f:id:toranoana-lab:20201223204510p:plain

まえがき

虎の穴ラボ、アドベントカレンダー最終日です。

虎の穴ラボCTOの野田が書きます。

この記事は、虎の穴ラボ Advent Calendar 2020の25個目の記事です。
昨日は、「チームビルディング」についてY.Mさんが書いてます、ぜひこちらもご覧ください。

qiita.com

虎の穴ラボのサイトのSPECIAL素材コーナー に、 Gopher君のサンタさんがいたので、クリスマス当日ということで本日はGo言語の話題で書きたいと思います。

動機と目的

アセンブラをやるならC言語のインラインアセンブラか、直接アセンブラを書けばいいのですが、 「普段触っている言語からCPUの機能を直接利用したいよね」という動機から、 今回はGo言語からcgo(※1)を使いC言語のインラインアセンブラを使った値のやりとりをしてみたいと思います。

※1 Go言語からC言語を利用するためのバインディング機能

今回の私の開発環境は以下です

  • macOS
  • Intel Core i5
  • Cコンパイラ (/usr/bin/gcc)※実態はClang
  • Go言語 1.13

1. (入門) cgoを使って、GoからC言語を呼び出す

まずcgoを体験するためにベースとなるファイルを用意します。

main.go

package main

/*
#cgo LDFLAGS: -L. -lhello
#include <hello.h>
*/
import "C"  

func main() {
    C.hello()
}

import "C" でcgoの機能を利用、コメントに見える部分でライブラリのリンクとインクルードファイルを指定します。

hello.c

#include <stdio.h>
#include "hello.h"

void hello(void)
{
    printf("Hello, World!\n");
}

C言語の関数ライブラリの部分になります。

hello.h

extern void hello(void);

定義した関数のヘッダーファイルになります。

これを以下の手順で結合します

// Cファイルをオブジェクトファイルへコンパイル
$gcc -c hello.c 
// オブジェクトファイルからCライブラリ作成
$ar rusv libhello.a hello.o
// 作ったCライブラリとGoファイルを結合してコンパイル
$go build main.go

実行すると

$./main
Hello, World!

無事Go言語から、C言語で定義した関数が実行できました!

2. cgoを使って、Goからアセンブリを呼び出し(cpuidの実行)

ここからが本題です、今回はGoから呼び出されたCファイルからIntel系CPU(x86)のcpuid命令を実行してみます。 cpuidとはCPUのスペック情報などを返すcpuの命令になります。詳細は下記に日本語で資料があります。

Processor_Identification_071405_i.pdf

さきほどのhello.cの中身を下記に変更します。

#include <stdio.h>
#include "hello.h"

void hello(void)
{
    unsigned int eax_val;
    unsigned int ebx_val; //Genu
    unsigned int ecx_val; //ntel
    unsigned int edx_val; //ineI
    char    vendor_name[16];
    asm volatile (
        "xor %%eax, %%eax\n\t"  //eaxは0になる
        "cpuid"
        : "=a"(eax_val),
         "=b"(ebx_val),
         "=c"(ecx_val),
         "=d"(edx_val)
    );
    printf("eax: %x\n", eax_val); //今回はあまり重要でないがcpuidの基本情報を利用するときにEAXに入れられる最大値
    memcpy(vendor_name, &(ebx_val), 4);
    memcpy(&vendor_name[4], &(edx_val), 4); // EDXが先なのに注意
    memcpy(&vendor_name[8], &(ecx_val), 4);
    printf("vendor name: %s\n", vendor_name);
}

C言語のインラインアセンブラとその拡張構文を使って、cpuid命令を実行します。 EAX、EBX、ECX、EDXの汎用レジスタを使い下記を行います。

  • [1] EAXに0を入れることでベンター名を取得する旨の引数を割当
  • [2] EAXが0なのでcpuidはベンター名を返すロジックに入る(0以外だとCPUクロック数などを返す)
  • [3] EBX、ECX、EDX にベンター名を格納

インラインアセンブラにはコンパイラごとに文法が違うので、 今回はあまり意識しなくても大丈夫ですがMacの標準のコンパイラがclangということは意識しておくとよいです。 インラインアセンブラの文献はgccが中心なので、ググってコピペするとエラーになることがあります。

コンパイルする前に一回作ったファイルは消しておきます。

rm main libhello.a hello.o

そして先程のコンパイルと同じ手順でgo buildします。

$gcc -c hello.c 
$ar rusv libhello.a hello.o
$go build main.go

実行します。

$./main
eax: 16
vendor name: GenuineIntel

Goからcgo経由でCのインラインアセンブラを呼び出し、CPU命令を使って「GenuineIntel(Intel純製)」というベンダー名が取得できました!

3. アセンブラから渡ってきた、C言語の戻り値をGo側で使う方法(cpuidの実行)

次に先程の「GenuineIntel」文字列をGo側で受け取って、Go側で出力していきたいと思います。 今回はすべてのファイルに変更を加えます。

main.go

package main

/*
#cgo LDFLAGS: -L. -lhello
#include <hello.h>
*/
import "C"

func main() {
   v:= C.hello()
   println(C.GoString(v)) //C言語文字列をGoの文字列に変換
}

hello.c

#include <stdio.h>
#include "hello.h"

char* hello(void) //戻りの型をC文字列に
{
    unsigned int eax_val;
    unsigned int ebx_val;
    unsigned int ecx_val;
    unsigned int edx_val;
    char    vendor_name[16];
    asm volatile (
        "xor %%eax, %%eax\n\t"
        "cpuid"
        : "=a"(eax_val),
        "=b"(ebx_val),
        "=c"(ecx_val),
        "=d"(edx_val)
    );
    printf("eax: %x\n", eax_val);
    memcpy(vendor_name, &(ebx_val), 4);
    memcpy(&vendor_name[4], &(edx_val), 4);
    memcpy(&vendor_name[8], &(ecx_val), 4);

    char* cp = NULL;
    cp = (char*)malloc(sizeof(char) * 16);
    strcpy(cp, vendor_name);
    return cp;
}

関数の戻り値をC文字列にし、戻り値の処理を追加しています。

hello.h

extern char* hello(void); //戻り値の型を変更

先程と同じ手順で、ファイルをクリーンしたあとコンパイルして実行します。

$rm main libhello.a hello.o
$gcc -c hello.c
$ar rusv libhello.a hello.o
$go build main.go
$./main
eax: 16
GenuineIntel

Go言語側で、アセンブリの結果を受け取り出力することができました!

Go言語とアセンブラが共同作業できた感じがでてきました。

4. Goから引数としてC言語に値を渡して、アセンブリ言語で処理(addl 論理加算命令の実行)

さて最後のステップとして、Go言語からcgo経由で値を渡し、 アセンブラで処理をさせてその結果をGoで受け取り出力するという一連の流れを組み立てましょう。

今回もすべてのファイルを書き換えます。

main.go

package main

/*
#cgo LDFLAGS: -L. -lhello
#include <hello.h>
*/
import "C"
import (
   "os"
   "strconv"
)

func main() {
   input, _:= strconv.Atoi(os.Args[1]) // 実行第1引数の数値を受け取り
   println(input)
   v:= C.hello(C.int(input))
   println(v)
}

第一引数を受け取り、それをGoの数値に変換し、さらにC言語の数値に変換してC言語の関数に渡します。

hello.c

#include <stdio.h>
#include "hello.h"

int hello(int input)
{
    int add_value = 2;
    int r = 0;
    // objdump -d hello.oすると実際にはレジスタを使った計算になっている
    asm volatile(
                "addl   %1, %2"
                : "=a"(r)
                :"r"(input),"r"(add_value)
               );
    return r;
}

今回はCPUの”addl”「論理加算命令」を使い、与えられた数値から2加算する処理を書きます。 インラインアセンブラの拡張構文を使っているため、簡易に書けていますが、 実際にobjdumpをかけるともう少し複雑な記述で実行されているのがわかります。

hello.h

extern int hello(int); // 戻り型と引数の型を変更

クリーン手順とコンパイル手順は前と同じです。

第一引数に数値をいれて実行します。

$ ./main 1
1  <- 入力した値
3  <- addlで2加算した結果
$ ./main 2
2
4
$ ./main 3
3
5

Goから渡された値をアセンブラで処理し、その結果をGoで処理することができました! この一連の流れを作り理解できれば、さらに複雑な連携処理を作り込むことができると思います。

まとめ

cgoを使い、C言語のインラインアセンブラを使ったGo言語からアセンブリを利用する方法を紹介しました。
記事を作成するにあたって元々はGo言語から直接アセンブラを実行しようと調査したのですが、Go言語にインラインアセンブラが無いことと、 直接アセンブラファイルを書いてGoのツールでオブジェクトファイルにするのも文献が少なすぎるので、cgoを使ってやるほうが楽という印象を受けました。

今回はIntel CPUを対象としましたが、最近はARMも流行っているのでARMのアーキテクチャマニュアルを 見て色々ためすのも冬休みの楽しみとして面白いかと思います。

"intelのアーキテクチャマニュアル"(でググればPDFの直リンに飛べる)

IA32_Arh_Dev_Man_Vol1_Online_i.pdf

ARMの本 (CPU命令セットなどの解説も豊富)

ARM組み込みソフトウェア入門―記述例で学ぶ組み込み機器設計のためのシステム開発 (Design Wave Advanceシリーズ) (日本語)

では皆様良いお年を

f:id:toranoana-lab:20201223212317p:plain

その他採用情報

虎の穴ラボでの開発に少しでも興味を持っていただけた方は、採用説明会やカジュアル面談という場でもっと深くお話しすることもできます。ぜひお気軽に申し込みいただければ幸いです。
カジュアル面談では虎の穴ラボのエンジニアが、開発プロセスの内容であったり、「今期何見ました?」といったオタクトークから業務の話まで何でもお応えします。
カジュアル面談や採用情報はこちらをご確認ください。
yumenosora.co.jp