[パタヘネ:読書メモ]付録B アセンブラ,リンカ,SPIMシミュレータ

B.1 はじめに

コンピュータが理解できるのは0/1の羅列である機械語のみだけど、機械語だと人が読みづらいので、アセンブリ言語を用意した。アセンブリ言語は、機械語と1:1に対応する名前を付けることで、人が読みやすくなる。この名前(命令)をニーモニックと呼ぶ。

アセンブリ言語を機械語に置き換えるために、アセンブラというプログラムを使用する。
素のアセンブリ言語だけだと、表現できる幅が狭いので、マクロ機能を設ける事が多い。

アセンブリのプログラム(.asm)は、アセンブリ->リンクという経緯を経て実行ファイルが作成される。

また、.asmファイルはニーモニック以外にも情報を記述できる場合が多い。ありがちなのは、.align .textや.grobalなど、ドットから始まるものでこれを、アセンブラディレクティブと呼ぶ。

ディレクティブ(指令)は、アセンブラプログラムに対する命令となる。例えば.alignは、実行ファイルが使用するメモリのアライメント調整を、アセンブラプログラムに指示している。


ニーモニックやマクロを使用しても、(機械語よりはましだけど)まだ人間にとっては読み書きしづらい。
これは、コンピュータが出来る事がプリミティブすぎるから。

この為、C言語などの、さらに抽象度が高い言語を用意する事になった。高水準の言語は、ループ(for/while)や分岐処理(if/switch)を、より分かりやすく記述する事ができ、高水準の言語はコンパイラを使用することでアセンブラのプログラムに翻訳される。

このように、コンパイラはある言語を別の言語に置き換えるフィルタプログラムになる。
ここで、置き換え元言語(.c等)をソース言語、置き換え先(.asm等)をターゲット言語と呼ぶ。


最近はアセンブリで直接プログラムを書く頻度は少ないが、ゼロというわけでは無い。
例えば、組み込み向けのプログラムでは、入力に対してレスポンスまでの遅延の猶予が少ない場合が多い(10ミリ秒以内のレベル)ため、生成コードが見えないC言語より、アセンブラのほうが動作に掛かる時間を試算しやすくなる。


実際のところは、大半をC言語で作り、一部をインラインアセンブラで書くという方法もある。
直接アセンブラでコーディングするのが一番実行効率がよいと思われがちだけど、近年のハードはキャッシュやパイプラインなどが発達しているので、素人(?)のアセンブラプログラマが作ったコードより、コンパイラが最適化したコードのほうが効率が良いことが多い。
但し、それでもプログラマは処理するデータの特性を知っているといったアドバンテージがあるので、その辺を考慮したコードが書ければ、コンパイラが生成したコードより高効率で処理する事も可能となってくる。

ただ…本当の実際のところは、低遅延が要求される部分でもC言語でコーディングする事は多い。
これは遅延時間が読めることよりも、”バグが少ないこと”が優先度が高いからで、アセンブリのコードは分かりにくいためバグが混入しやすい。


アセンブリの欠点としては、以下のものがあげられる。

抽象度が低いのでコーディングし辛い
  これは説明済み。
  また、C言語でコーディングするより、アセンブラのほうがコード量が
  長くなる為、開発の生産性は低くなってしまう。
 
マシン(アーキテクチャ)依存のコードになってしまう。
  作った資産を流用しにくい




B.2 アセンブラ

アセンブラプログラムは、.asm(アセンブラソース)を元に、.obj(オブジェクトファイル)を出力する。

アセンブルは大きく見ると以下の流れて行われる。

ラベルを見つけて、それに対応する命令を対応付ける。
各ニーモニックを翻訳する。
ラベル情報、翻訳結果、オペランド等の情報を元に、機械語に置き換える。



ラベルには、単一のasmファイルで完結するものと、他ファイルからも参照されるものがある。前者(ローカルラベル)はアセンブルの過程で解決されるが、後者(グローバルラベル/外部ラベル)は解決されないのでそのことをobjファイルに記録しておく必要がある。


アセンブラの場合、プログラムの位置(実際の行番号的な意味で)的に見て、ラベルの定義場所よりも、参照している場所のほうが上にくることがある。これは、例えば後ろのほうの場所へのジャンプ命令などが相当する。このような状態を前方参照と呼ぶ。
という事は、アセンブラプログラムはファイルを1回読むだけではアセンブルを確定する事が出来ない。
まず、1回目の読み込みでラベルを調べラベル一覧を作る。その後、改めて2回目の読み込みで翻訳作業をする事になる。
前方参照が発生する言語では、複数回の読み込みが必要となり、これをマルチパスの処理と呼ぶ。
一方、C言語等の場合では、使用するモノは事前に宣言が必要というルールになっているのでこのようなことは発生しない。このようなものを(マルチパスではなく)ワンパスのコンパイラと呼んだりする。


マルチパスの場合、最初のパスではプログラムのparseを行う。
parseの途中でラベルが出てきた場合は、シンボルテーブルにその情報を格納する。
同時にこれまでに出てきた各命令の長さを調べておき、ラベルのアドレスを特定する。

2回目のパスでは、ファイルを再度上から読んでいくが、ラベルが出てきた時はシンボルテーブルより、具体的な相対アドレス値に置き換える。分割アセンブルでリンク時に確定するシンボル情報は、ここでは分からないので未解決としてシンボルテーブルに残ったままになる。


UNIXの場合、objファイルの形式は以下のようになる。

オブジェクトファイルヘッダ
  各セグメントの大きさ情報等を記録する
 
テキストセグメント
  プログラムの機械語命令が入る
 
データセグメント
 
リロケーション情報
  絶対アドレスに依存する情報を記録する。
 
シンボルテーブル
  未解決の参照情報が記録される。
 
デバッグ情報
  ソースプログラムの行番号やソースファイル名等の、コンパイル時の情報が記録される



アセンブラは、プログラムが何番地にロードされるか分からないので、.objファイル上では0番地から始まるなどの仮定を置く事が多い。この情報はリンク時に変更される場合があるが、分岐命令などで分岐先アドレスを相対値で指定するようなアーキテクチャの場合は、変更の必要が無いので楽チンになる。


B.3 リンカ


リンカは、1つ以上の.objを結合させ、未解決のアドレスを解消するのと、objで決まっていた相対アドレスを絶対アドレスに調整するのが主な目的となる。

外部参照先は、自分が作ったほかのプログラムだけとは限らず、用意されているライブラリ関数の場合も有りうる。ライブラリ関数をコールしている場合は、実行ファイルに該当関数のコードが埋め込まれる。

リンカの生成物は実行ファイル(windowsの場合は.exe)となり、実行可能となる。


B.4 ロード


実行ファイルはOSによってメモリにロードされ実行することが出来る。
(OS lessの場合は、CPU起動と同時に実行される)

UNIXの場合、実行ファイルは一般的に以下のような流れで実行される。

実行ファイルより、テキストセグメント/データセグメントの領域を求め、メモリ上にエリアを確保する。
エリアは他にスタック領域などのデータ領域を含む。
 
テキストセグメントの情報(プログラム)をメモリにロードし、スタックに起動引数(argv)を積む。
 
レジスタをクリアする。
 
プログラムカウンタをスタートアップルーチンにセットする。
 
スタートアップルーチンからmain()がコールされる。




B.5 主記憶領域の使用法

この節では、MIPSで上で動作するプログラムがメインメモリをどのように使用するかについてのガイドラインを示す。ガイドラインは、ハード的な制約ではなく、単なる取り決め(慣習)という位置づけなので破る事は(理論上は)可能となっている。

MIPSではメモリの使い道を大きく4つに分ける。

予備(0x00900000~0x00400000)
 
テキストセグメント(0x00400000~0x10000000)
	プログラムのコードが入る
 
データセグメント(0x10000000から上位方向へ)
	0x10000000から上位方向に向かって、使用する
	C言語で言うところの、文字列リテラル、static変数、グローバル変数などの"静的データ"が格納される。
	"静的データ"の直ぐ後ろに、他にmallocで動的に確保した領域が確保される。
 
スタックセグメント(0x7fffffffから下位方向へ)
	0x7fffffffから下位方向に向かって使用する
	ここには関数のコールスタックが格納される



データセグメントと、スタックセグメントは同じ空間を、両端からそれぞれ相手の方向に向かって確保していく。なので、一方が領域を使いすぎると、もう一方が十分なメモリを使えないという場合があり得る。


関連記事

コメントを残す

メールアドレスが公開されることはありません。