前回の続きです。
本記事では、前記事で書いたハローワールドを読み解くことを目標とします。
ほとんど、低レベルプログラミングを2章までのまとめに近い内容になってるはずです。
また、付属している設問にも回答していこうと思います。
レポジトリはここです。
github.com
目次
- 第1章 「コンピュータアーキテクチャの基礎」のまとめ
- ハローワールドの解説
1. 第1章 「コンピュータアーキテクチャの基礎」のまとめ
前回は、実行環境の構築のみで各専門用語の説明を全くしていませんでした。
解説に入る前に、ちょうど第 1 章の設問が用語のまとめになりそうなので、本記事ではそこから始めようと思います。
また書籍にある、全設問を記載しているわけではないのでご注意ください。
第1章の問題と回答
フォン・ノイマン・アーキテクチャの主な原則は?
1章では、フォン・ノイマン・アーキテクチャの主要な機能として以下が述べられている。
- 0 と 1 で表されるビット(bit)という情報単位のみがメモリに保存される
- 命令とデータが区別されることなくメモリに保存される
- メモリはラベルによってインデックスが付与された、複数の cell によって組織化されている
- 特別な命令をのぞいて、プログラムは逐次的にフェッチされる命令軍で構成されている
レジスタとは?
CPU に直接備わっているメモリセルのこと。
レジスタにより、CPU とメモリ間のデータ交換時に生じる CPU タイムを削減できる。
ハードウェアスタックとは?
2つのマシン語命令(push と pop)と1個のレジスタ(rsp)によって実装された、スタックを実現するエミュレーションのこと。
割り込みとは?
外部イベントを基準としてプログラムの実行順序を変更すること。ゼロによる除算なども割り込みによって特別なルーチンを実行する。
フォン・ノイマンのモデルの主な問題点で、現在の拡張が解決しているのは?
- メモリへの問い合わせが必須だった問題をレジスタによって解決
- 対話性がなかった問題を割り込みによって解決
- コードを効果的に隔離できなかった問題をハードウェアスタックにより解決
- プログラムがどんな命令でも実行できてしまう問題をプロテクションリングによって解決
- プログラムそのものを互いに隔離できなかった問題を仮想メモリによって解決
スタックポインタの目的は?
ハードウェアスタックのもっとも上にある要素のアドレスを格納すること。
スタックは空になるか?
ならない。push していなくても pop は実行可能であり、何らかの値を返す。
スタック内の要素は数えられるか?
不可能。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
です。
global
もsection
やdb
と同じくディレクティブであり、プログラムの実行を開始するアドレスを指定します。
_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
1
はwrite
、60
はexit
を意味しており、mov によって、rax
レジスタに格納されています。
どちらもrax
レジスタに値を格納しているのは、システムコールを実行するために以下の手順を踏む必要があるからです。
- rax レジスタにシステムコールの番号を入れる
- システムコールが使用する引数は、
rdi
,rsi
,rdx
,r10
,r8
,r9
のいずれかに格納する(これ以上の引数、つまり6個以上の引数を受け取ることはできない)
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 ; システムコールを実行
うるさいくらいコメントを記述しましたが、これでわからない部分がなくなりました。
まとめ
前記事で記述したハローワールドのアセンブリを解説しました。
前回の記事で更新してなかったら死んでるとかどうの言ってましたが、生きてました。