linux でx86アセンブル(IA-32 x86)

bullseye 2022.09.13
bullseye 2022.12.28


1 インストール

$ sudo apt-get install nasm

2 hello, worldを試す

言語学習の第1歩の定番

(1)ソースプログラム

hello.asm
section .text
global _start

msg db  'hello,world', 0x0A
msglen  equ     $ - msg

_start:
        mov ecx, msg
        mov edx, msglen
        mov eax, 4
        mov ebx, 1
        int 0x80
        mov eax, 1
        mov ebx, 0
        int 0x80

(2)アセンブル

$ nasm  -f elf hello.asm

 ただし、64bit の場合は
$ nasm -f elf64  hello.asm
でアセンブルしないとリンクでエラーになる。

 無事にアセンブルが終わるとhello.oというオブジェクトプログラムができる。

$ ls  -l で確認してみよう。


(3)リンク


 $ ld -o hello01 hello.o

 実行ファイル hello01 ができる。ldの書式は次のとおり
   ld  -o 実行形式ファイル名   リンクするオブジェクトファイル1  リンクするオブジェクトファイル2・・・・

(4) 実行方法は


 $ ./hello01


(5)実行ファイルのダンプ

  $    od -A x -t x1z -v  hello01

(6)逆アセンブル

  ndisasm コマンドを使う

$ ndisasm   hello.o > hello.disasm

 結果がdisasmに入る.


ちなみにc言語でhello,worldを表示させたプログラムをアセンブラに変換する手順を記述しておく
hello.c

#include<stdio.h>
void main(){
    printf("hello,world\n");
}

普通にコンパイルして実行ファイルhello02を出力するには
 gcc  hello.c  -o   hello02

逆アセンブルするには
 gcc main.c -g -c
でコンパイルするとオブジェクトファイルmain.oが作成されるのでこれを
 objdump main.o  -S
で見ることができる。
 

4 複数のオブジェクトファイルのリンク

 他のオブジェクトファイルのサブルーチンを使うには
   extern サブルーチン名
 で呼べるようにする。
 逆に呼ばれる方は 
   global  サブルーチン名
 でグローバル宣言をしておく

 次の例は2バイトカウンタの例で単純にcxレジスタをインクリメントして標準出力するプログラムである。
 2バイトなので16進数4桁を文字コードにするサブルーチンを別ファイルに分けた。(使ってないけど、
 文字コードを数値化するソースも入っている)
  なお途中でcxをpush,popで退避、復帰しているが、64bitOSではなぜ かこれがうまく働かないので
 movでどこかに退避した方がいいみたい。

 ex08.asm (メインルーチン)   
;***********************************
; Assembly Programming on X86 Linux
;  cxレジスタを16bitのカウンタとして扱う
; c3番地からc0番地までをcxレジスタの数値表示用の文字コードを格納し
; 16進数で画面に表示する  
;***********************************
section .data
c3:     db  0x00 ; cxレジスタ3桁目の文字コード
c2:     db  0x00 ; cxレジスタ2桁目の文字コード
c1:     db  0x00 ; cxレジスタ1桁目の文字コード
c0:     db  0x00 ; cxレジスタ0桁目の文字コード
        db  0x0A ; 改行コード
        msglen        equ $-c3

section .text ; テキストセクションにはプログラムを記述

        extern  _n2char  ;alの16進数1桁を文字コードにする
        global  _start 
_start:           ; プログラム先頭ラベル
    xor    cx,cx ; cxレジスタ0クリア
label1:
    inc    cx ; inc命令ではcary フラグはonにならないので注意
    jz    label2

    mov    ax,cx
    call    _n2char
    mov    [c0],al

    mov    al,ah
    call    _n2char
    mov    [c2],al

    mov    ax,cx
    ror    ax,4
    call    _n2char
    mov    [c1],al
   
    mov     al,ah
    call    _n2char
    mov     [c3],al

    push    ecx     ; ecx退避
    mov     ecx,c3 ; 出力するデータの先頭アドレスをecxにセット
    mov     edx,msglen ; 出力するデータの長さをedxにセット
    mov     eax,4 ; システムコールwrite()の番号をeaxにセット
    mov     ebx,1 ; ファイルディスクリプタを標準出力にする
    int     0x80  ; システムコール
    pop     ecx   ; ecx復帰
    jmp    label1
label2:
    mov     eax,1 ; システムコールclose()の番号をeaxにセット
    mov     ebx,1 ; クローズするファイルディスクリプタの番号
    int     0x80  ; システムコール
 
 c_x_n.asm
section .text
        global _n2char
        global _c2num
;*****************************************************
; 機能ALレジスタ 0からfまでの16進数を文字コードにする
; 入力AL    出力AL
; 例1. 入力AL=0x05  出力AL=0x35
; 例2. 入力AL=0x0A  出力AL=0x61
;****************************************************    
_n2char:
        and     al,0x0F ; 上位4bit0クリア
        cmp     al,0x09 ; al - 0x09
        jg      n2_1    ; al > 0x09 then n2_1
        or      al,0x30 ;
        ret
n2_1:
        sub     al,0x09
        or      al,0x60  
        ret
;*****************************************************
; 機能ALレジスタ 数値0からfまでの文字コードを数値にする
; 入力AL    出力AL
; 例1. 入力AL=0x35  出力AL=0x05
; 例2. 入力AL=0x41  出力AL=0x0a
; 例2. 入力AL=0x61  出力AL=0x0a
;****************************************************    
_c2num:
        push     ax
        and      al,0xf0
        cmp      al,0x30
        je       c2_1
        cmp      al,0x40
        je       c2_2
        cmp      al,0x60
        je       c2_2
        pop      ax
        ret
c2_1:
        pop      ax
        and      al,0x0f
        ret
c2_2:
        pop      ax
        and      al,0x0f
        add      al,0x09
        ret

アセンブル
 nasm -f elf  ex08.asm
 nasm -f elf  c_x_n.asm
リンク
 ld  -o counter  ex08.o  c_x_n.o


実行
 ./counter



5 c言語のプログラムから利用するには

 ia-32ではC言語からの引数は、stackエリアを介して行われる。
 (ia-64では第1引数から第6引数まで順にedi,esi,edx,ecx,r8d,r9dが使われ、もしさらに引数が
あるときはスタックエリアが使われる。)
 戻り値はeaxを介して戻される。

例 c言語のmainプログラムからアセンブラの減算モジュールをコールする

cのmainプログラム(ex10.c)
#include <stdio.h>
extern int asm_sub(int a, int b);

int main(){
    int a, b;
    a = 3; /*第一引数*/
    b = 2; /*第ニ引数*/
    int ans = asm_sub(a, b);
    printf(”%d\ n”, ans);
    return 0;
}
コンパイル
gcc  -c   -o  ex10.o  ex10.c     (-cオプションはリンクを行わずオブジェクトファイルを作成する)

アセンブラのプログラム(sub.asm)
section .text
global asm_sub

asm_sub:
    mov   eax,[esp+4] ; 第 1 引数を eax へ
    sub   eax,[esp+8] ; eaxから第2引数を減算 , 戻り値は eax
    ret
 上の場合、スタックエリアは
 [esp]   retで戻るアドレス
 [esp+4] 第1引数
 [esp+8] 第2引数

アセンブル
nasm -f elf sub.asm

実行形式のファイルの作成
gcc  -o  sub  ex10.o   sub.o

実行
./sub

6 補足

 c言語で記述したプログラムの方に#include<math.h>があるときは、リンクのとき -lm オプションを最後につける
 アセンブラで記述したプログラムの方のsection .dataにワーキングエリアを確保したときは -no-pie オプションをつける

 例
  cのプログラム prime05.c
/*********************************************
 * 2147300001から2147483647
 * の間の素数を求める
 * ******************************************/
#include<stdio.h>
#include<time.h>
#include<math.h>

extern int p_chk(int a,int s);
/* 判定値=p_chk(int a,int s);*/
/* a=判定したい素数 s=素数の平方根の整数部 */
/* 判定値1 素数,0 素数以外*/

void main(){
    long cpu_time;
    double sec;
    int    s;
    int    p;
    int a=2147300001;
    while(a >=0){
        s=(int)sqrt(a);
        p=p_chk(a,s);
        if(p!=0){
            printf("%d\n",a);
        }
        a++;
    }

    /*cpu時間をチェック*/
    cpu_time=clock();
    /*秒に変換*/
    sec=(double)cpu_time /CLOCKS_PER_SEC;
    printf("%f秒\n",sec);
}
 コンパイルは gcc  -c -o prime05.o prime05.c

アセンブラのプログラム p_chk2.asm
;*******************************************************
; 素数チェックモジュール
; 入力 第一引数[esp+4] 素数判定する数
;      第ニ引数[esp+8] 素数判定する数の平方根の整数部
;      [esp+4] / [esp+8]
;    
; 出力 判定 eax !=0 素数  0 素数ではない
;*******************************************************
section .data
kensa_no:  dd 0 ; 第1引数(検査する数)
sqrt_no:   dd 0 ; 第2引数 (第1引数の平方根)
section .text
global p_chk

p_chk:
    mov    ecx,2
    mov    eax,[esp+8]
    mov    [sqrt_no],eax
    mov    eax,[esp+4]
    mov    [kensa_no],eax
label1:
    mov    eax,[kensa_no] ;第1引数をeaxへ
    xor    edx,edx
    div    ecx
;   商   eax = edx:eax / ecx
;   剰余 edx
    cmp    edx,0
    jz     label2
    inc    ecx
    cmp    ecx,[sqrt_no]
    jbe    label1
label2:
    mov       eax,edx ;(余りをeaxにセット)
    ret
アセンブル方法は nasm  -f elf  p_chk2.asm

リンクでオプションをつけ忘れると
$ gcc -o prime prime05.o p_chk2.o
下のメッセージがでる
/usr/bin/ld: p_chk2.o: warning: relocation in read-only section `.text'
/usr/bin/ld: prime05.o: in function `main':
prime05.c:(.text+0x34): undefined reference to `sqrt'
/usr/bin/ld: warning: creating DT_TEXTREL in a PIE
collect2: error: ld returned 1 exit status

解決策は -lm と -no-pie をつけてリンクする

gcc -o prime prime05.o p_chk2.o -lm  -no-pie