MATHGRAM

主に数学とプログラミング、時々趣味について。

【低レベルプログラミング】アセンブリ言語【その2】

前回の続きです。

本記事では、前記事で書いたハローワールドを読み解くことを目標とします。 ほとんど、低レベルプログラミングを2章までのまとめに近い内容になってるはずです。 また、付属している設問にも回答していこうと思います。

レポジトリはここです。

github.com

目次

  1. 第1章 「コンピュータアーキテクチャの基礎」のまとめ
  2. ハローワールドの解説

1. 第1章 「コンピュータアーキテクチャの基礎」のまとめ

前回は、実行環境の構築のみで各専門用語の説明を全くしていませんでした。 解説に入る前に、ちょうど第 1 章の設問が用語のまとめになりそうなので、本記事ではそこから始めようと思います。 また書籍にある、全設問を記載しているわけではないのでご注意ください。

第1章の問題と回答

  1. フォン・ノイマンアーキテクチャの主な原則は?

    1章では、フォン・ノイマンアーキテクチャの主要な機能として以下が述べられている。

    • 0 と 1 で表されるビット(bit)という情報単位のみがメモリに保存される
    • 命令とデータが区別されることなくメモリに保存される
    • メモリはラベルによってインデックスが付与された、複数の cell によって組織化されている
    • 特別な命令をのぞいて、プログラムは逐次的にフェッチされる命令軍で構成されている
  2. レジスタとは?

    CPU に直接備わっているメモリセルのこと。 レジスタにより、CPU とメモリ間のデータ交換時に生じる CPU タイムを削減できる。

  3. ハードウェアスタックとは?

    2つのマシン語命令(push と pop)と1個のレジスタrsp)によって実装された、スタックを実現するエミュレーションのこと。

  4. 割り込みとは?

    外部イベントを基準としてプログラムの実行順序を変更すること。ゼロによる除算なども割り込みによって特別なルーチンを実行する。

  5. フォン・ノイマンのモデルの主な問題点で、現在の拡張が解決しているのは?

    • メモリへの問い合わせが必須だった問題をレジスタによって解決
    • 対話性がなかった問題を割り込みによって解決
    • コードを効果的に隔離できなかった問題をハードウェアスタックにより解決
    • プログラムがどんな命令でも実行できてしまう問題をプロテクションリングによって解決
    • プログラムそのものを互いに隔離できなかった問題を仮想メモリによって解決
  6. スタックポインタの目的は?

    ハードウェアスタックのもっとも上にある要素のアドレスを格納すること。

  7. スタックは空になるか?

    ならない。push していなくても pop は実行可能であり、何らかの値を返す。

  8. スタック内の要素は数えられるか?

    不可能。7. と同じ理由で pop は任意の回数実行できる。そのため要素数を数えることはできない。

以上が、第1章の問題と回答です。 知っている人にとってはかなり当たり前の内容だと思いますが、これで一旦用語が整理できました。 それではこれらの用語を使いつつ、前記事で扱ったハローワールドを紐解いて行きましょう。

2. ハローワールドの解説

まずはハローワールドを表示するアセンブリを再掲します。

section .data
message: db 'hello, world!', 10

section .text
global _start

_start:
  mov rax, 1
  mov rdi, 1
  mov rsi, message
  mov rdx, 14
  syscall

  mov rax, 60
  xor rdi, rdi
  syscall

それでは、まずそれぞれの記述が何を意味しているのかに注目して、上から順に紐解いてみましょう。

※ 注意: この書籍で扱っているアセンブリは NASM です。GAS とは異なるので注意してください!

2.1. 文法編

2.1.1. section

まずは1行目にあるsection から。

前節で説明したように、

命令とデータが区別されることなくメモリに保存される

というのがフォン・ノイマン型の主要な機能としてあげられます。 そのため、プログラマが命令とデータを簡易的に区別できるように用いられるのがセクションです。

1行目には

section .data

と記述されていますが、section .dataグローバル変数を記述するためのセクションであることを意味します。 一方、4行目にあるsection .textは命令を記述するセクションを意味します。

セクションは機械語コンパイルされず、コンパイル時の補助的な役割を担います。 このように直接機械語に変換されず、変換処理を制御する要素をディレクティブと呼びます。

2.1.2. label

次に2行目

message: db 'hello, world!', 10

で使われている message: について。これはラベルと呼ばれます。 ラベルを用いることで、プログラマがわかりやすい名前をアドレス値に付与することができます。

高級言語の変数に似ている概念ですが、アセンブリでは変数や手続きが厳密に区別されないため、ラベルという言葉を用いるのが一般的だそうです。

参考: NASM Manual: Layout of a NASM Source Line

また、_startもラベルです。 アセンブリは複数のファイルに分けて書くことが可能ですが、どこの処理から始めるかを宣言して上げる必要があります。その時に用いられるのがこの_startラベルです。

main()関数みたいなものですかね。

2.1.3. db

同じく二行目のdbについて。これも、section と同様にディレクティブの一種です。 db ディレクティブはバイトデータを初期化するために用いられます。

つまり、以下のように記述することで、文字列hello, world!に対応する ASCII コードと、改行を示す特殊コードの10が、messsageラベルに格納されます。

message: db 'hello, world!', 10

db の他にもワードデータを初期化するためのdwやダブルワードを初期化するためのddなどが存在します。 詳しくは以下を参照してください。

参考: NASM Manual: DB and Friends: Declaring Initialized Data

2.1.4. global

次に、5 行目

global _start

にあるglobalです。 globalsectiondbと同じくディレクティブであり、プログラムの実行を開始するアドレスを指定します。 _startはラベルであり、8行目以下の命令群が格納されています。

つまりこのプログラムは_startの先頭に記述されているmov rax, 1から実行されることを意味します。

2.2. 命令編

ここまでの解説で、とりあえず各コマンドの意味は掴めてきたと思います。

次に、プログラムの主役である、命令部分を詳しく見て行きましょう。

命令部分のみを再掲します。

mov rax, 1
mov rdi, 1
mov rsi, message
mov rdx, 14
syscall

mov rax, 60
xor rdi, rdi
syscall

文法編と同様に一つずつ紐解いて行きます。

2.2.1 mov

mov とは、ある値をレジスタかメモリに書き込むために用いる命令です。 高級言語で「代入」として扱っている操作が近いです。

mov には以下のルールが存在します。

  • メモリからメモリへの移動はできない。
  • 移動元と移動先のオペランドのサイズが同じでなければならない。

mov を使って、システムコールやラベルなどが示す様々な値をレジスタに格納します。

2.2.2. syscall

syscall とは、*nix システムでシステムコールを実行するために用いる命令です。システムコールには様々な種類が存在し、一意に値が定義されています。

上のプログラムでは以下の行の1, 60システムコールを表しています。

mov rax, 1
mov rax, 60

1write60exitを意味しており、mov によって、raxレジスタに格納されています。

どちらもraxレジスタに値を格納しているのは、システムコールを実行するために以下の手順を踏む必要があるからです。

  1. rax レジスタシステムコールの番号を入れる
  2. システムコールが使用する引数は、rdi,rsi,rdx,r10,r8,r9のいずれかに格納する(これ以上の引数、つまり6個以上の引数を受け取ることはできない)
  3. syscall命令を実行する。

もう一度ハローワールドのプログラムを見てみましょう。

mov rax, 1 ; raxレジスタにwriteを格納
mov rdi, 1 ; rdiレジスタに1つ目の引数として、1を格納
mov rsi, message ; rsiに2つ目の引数として、messageを格納
mov rdx, 14 ; rdxに3目の引数として、14を格納
syscall ; syscallを実行

以上のように、システムコールを実行する手順をちゃんと踏んでいたことがわかると思います。

また、ここで linux の write システムコールのドキュメントを読んでみます。 https://linuxjm.osdn.jp/html/LDP_man-pages/man2/write.2.html

ssize_t write(int fd, const void *buf, size_t count);
write() は、 buf が指すバッファーから、ファイルディスクリプター fd が参照するファイルへ、最大 count バイトを書き込む。

このドキュメントの通りに先ほどのプログラムをみてみると、以下の操作を行なっていたことがわかります。

  • 1つ目の引数でファイルディスクリプタを指定
  • 2つ目の引数でバッファのアドレス(書き込むバイト列の先頭の値)を指定
  • 3つ目の引数で書き込むバイト数を指定

このプログラムでファイルディスクリプタ1を指定していますが、これはstdoutを示すもので「hello, world!」をターミナルに表示するための命令になります。

2.3 まとめ

それではここまで解説したことを念頭におきながらもう一度、プログラムを眺めてみます。

; .dataセクション。以下にglobal変数を定義することを宣言
section .data
; dbディレクションを用いて、
; 'hello, world!'と改行文字を示す10のバイト列を、
; messageラベルとして定義
message: db 'hello, world!', 10

; .textセクション。以下に命令を記述することを宣言
section .text
; _startラベルをgloabalディレクティブで宣言
global _start

; _startラベルとして以下の命令群を定義
_start:
  mov rax, 1 ; システムコールwriteをraxに格納
  mov rdi, 1 ; ファイルディスクリプタの値をrdiに格納
  mov rsi, message ; messageラベルの中身をrsiに格納
  mov rdx, 14 ; バイト数をrdxに格納
  syscall ; システムコールを実行

  mov rax, 60 ; システムコールexitを60に格納
  xor rdi, rdi ; 同値のxorをとってrdiの値を0に
  syscall ; システムコールを実行

うるさいくらいコメントを記述しましたが、これでわからない部分がなくなりました。

まとめ

前記事で記述したハローワールドのアセンブリを解説しました。

前回の記事で更新してなかったら死んでるとかどうの言ってましたが、生きてました。

【低レベルプログラミング】docker による実行環境構築とハローワールド 【その1】

めっちゃお久しぶりです.

エンジニアとして強くなるために低水準言語をちゃんと理解したくなり, 最近低レベルプログラミングを購入しました.

低レベルプログラミング

低レベルプログラミング

初心者には割と敷居が高い印象を受けましたが,知らない単語は調べつつ読み進めています.

そして忘れないためにも,この本から学んだことを記録として残していこうと思います.

シリーズ化しますので続いてなかったら挫折したか死んだかのどっちかだと思ってください.

本記事の内容は全て以下のレポジトリに push していますので,適宜参考にしてみてください.

また,この記事の本文も上のレポジトリに push しています. もし間違いや読みにくい文章等がございましたら issue を立てるなり PR を上げるなりしていただけると泣いて喜びます.

よろしくお願いします.

目次

  1. docker を用いた実行環境構築
  2. 簡単なアセンブリを書いてハローワールド

本記事では実行環境構築に焦点をあて, アセンブリの詳細な解説は次の記事で行います.

1. docker を用いた実行環境構築

この本ではDebian GNU\Linux 8.0の環境を推奨されていますが, いつも触っている Macbook で実行したいので Docker を使って環境を整えます.

目標とする実行環境は以下です。

まずは Dockerfile を作成します. 書籍の実行環境となるべく揃えたいのでイメージはdebian:8を使用しましょう.

C 言語のコンパイラである gcc 等も後々必要になるようですが,最初はミニマムで進めていこうと思います.

FROM debian:8

RUN apt-get update \
  apt-get install -y binutils nasm gdb \
  apt-get install -y vim

それではこの Dockerfile から docker イメージを作成しましょう.

$ docker build -t low-level-programming .

これでイメージが作成されました. 以上で実行環境構築は終了です.
ホントに docker は最高ですねー

2. 簡単なアセンブリを書いてハローワールド

早速ハローワールドを出力するアセンブリを書いてみましょう. と,言っても書籍のコード丸写しです.

元コードは書籍の p28 のリスト 2-4 です.

section .data
message: db 'hello, world!', 10

section .text
global _start

_start:
  mov rax, 1
  mov rdi, 1
  mov rsi, message
  mov rdx, 14
  syscall

  mov rax, 60
  xor rdi, rdi
  syscall

現状,高級言語しか知らない自分には本当に何が書いてあるかわからないです笑

冒頭でも書きましたが,このコードの解説は次の記事で行います.

では,アセンブル*1して実行してみましょう.

先ほど作った実行環境にアセンブリを保存しているワーキングディレクトリをマウントします.

$ docker run -it -v $(pwd):/work low-level-programming bash

あとはコンパイルして実行するのみです.

$ cd work
$ nasm -felf64 hello.asm -o hello.o
$ ld -o hello hello.o
$ ./hello

はい,以下のようにちゃんとハローワールドが出力できましたね.

root@97e9cd2fd7da:/work# ./hello
hello, world!

まとめ

  • 手を動かすために docker で実行環境を整えました.
  • アセンブリhello, world!を出力しました.

以上です.

次回は本記事で書いたハローワールドを読み解いていきます.

参考

本記事の内容は以下の記事を大いに参考にしています. 有益な記事ありがとうございました.

書籍「低レベルプログラミング」アセンブリ実行 Docker 環境の構築

1: アセンブラ機械語コンパイルすること

t-SNEの結果をplotlyで3D可視化する

前回のplotlyの記事で実践編は暇あったら書きます的なこと言ったのですが,今回はそれに当たる内容です.

内容量はかなり少なく薄いですが,plotlyの使用例程度に思ってくれると有難いです.

t-SNEとは

t-SNEとは,皆さまご存知の通り次元圧縮の手法ですね.高次元データを人間が認知できる次元まで綺麗に落とし込める手法なので使っている人は多いのではないでしょうか.

今回はplotlyの使い方を重視した記事なので,理論の話はしませんが需要があったらまとめますね.一応参考になる資料をここにまとめておきます.

論文:
https://lvdmaaten.github.io/publications/papers/JMLR_2008.pdf

参考サイト:
理論とか置いといてt-sneをアプリケーションとして使う人は読むべき
高次元のデータを可視化するt-SNEの効果的な使い方 - DeepAge

ざっと理論の概要を知りたい人はこちらで.
t-SNE を用いた次元圧縮方法のご紹介 | ALBERT Official Blog

実践してみる

上のt-SNE を用いた次元圧縮方法のご紹介 | ALBERT Official Blogでやってることをpythonに移植する流れでやりたいと思います.

データセットcoil-20を使用し,sklearnに実装されているtsneを用います.

まずは3D

まずは3次元まで落とし込んでみましょう.

import os
import numpy as np
import cv2
from sklearn.manifold import TSNE
from sklearn import preprocessing

import plotly.offline as offline
import plotly.graph_objs as go
offline.init_notebook_mode()

# 画像の前処理.標準化やらL2正規化やら.
def preprocess_image(path, size):
    img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
    resized = cv2.resize(img, (size, size), cv2.INTER_LINEAR).astype("float")
    normalized = cv2.normalize(resized, None, 0.0, 1.0, cv2.NORM_MINMAX)
    timg = normalized.reshape(np.prod(normalized.shape))
    return timg/np.linalg.norm(timg) 

ROOT = "./coil-20-proc"
ls = os.listdir(ROOT)

# 名前からラベルを持って来ます.
obj_ls = [name.split("_")[0] for name in ls]

ALL_IMAGE_PATH = [ROOT+"/"+path for path in ls]

# 全画像に対して前処理する
preprocess_images_as_vecs = [preprocess_image(path, 32) for path in ALL_IMAGE_PATH]

# tsneを実行
tsne = TSNE(
    n_components=3, #ここが削減後の次元数です.
    init='random',
    random_state=101,
    method='barnes_hut',
    n_iter=1000,
    verbose=2
).fit_transform(preprocess_images_as_vecs)

たったこれだけで次元削減できてしまいます.sklearnに感謝です.

さて,現在tsneという変数に次元削減後のarrayが入っているのでコイツをplotlyを用いて可視化してみます.

# 3Dの散布図が作れるScatter3dを使います.
trace1 = go.Scatter3d(
    x=tsne[:,0], # それぞれの次元をx, y, zにセットするだけです.
    y=tsne[:,1],
    z=tsne[:,2],
    mode='markers',
    marker=dict(
        sizemode='diameter',
        color = preprocessing.LabelEncoder().fit_transform(obj_ls),
        colorscale = 'Portland',
        line=dict(color='rgb(255, 255, 255)'),
        opacity=0.9,
        size=2 # ごちゃごちゃしないように小さめに設定するのがオススメです.
    )
)

data=[trace1]
layout=dict(height=700, width=600, title='coil-20 tsne exmaple')
fig=dict(data=data, layout=layout)
offline.iplot(fig, filename='tsne_example')

こんな感じで出力されます.グリグリ動かしてみてください.とても綺麗に分離できていることがわかります.尚,円形にplotされていることに関する考察などは先のブログでされているので是非参考にしてみてください.

2Dもやってみる.

ほぼ上と同じようにtsneを実行し,Scatter2dを用いて可視化してみます.

# tsneには2dまで落とし込んだarrayが入っている想定です.

trace = go.Scatter(
    x=tsne[:,0],
    y=tsne[:,1],
    mode='markers',
    marker=dict(
        sizemode='diameter',
        color = preprocessing.LabelEncoder().fit_transform(obj_ls),
        colorscale = 'Portland',
        line=dict(color='rgb(255, 255, 255)'),
        opacity=0.9,
        size=4
    )
)

data=[trace]
layout=dict(height=800, width=800, title='coil-20 tsne exmaple 2D')
fig=dict(data=data, layout=layout)
offline.iplot(fig, filename='tsne2D_example')

f:id:ket-30:20170705035109p:plain:w500:h500

この世の生データに比べたらかなり綺麗に分かれていますが,若干バラついてる部分も見受けられますね.やはり3Dと2Dでは元のデータに対する説明量が違いますので,この程度の差は出てしまいます.

内容としては以上です.

あとがき

plotlyは最近かなり使っているのですが,せっかく使っているのに実践編として記事にできてない状況になっています・・・.これからは,今回くらいの内容の薄さでもいいやぁって開き直って記事の更新頻度を上げていきたいと思います.

次はディリクレ分布を可視化してみようと思います.よろしければそちらも是非.

以上です.