ブログのネタ切れ対策の2番目で、今回は、C言語とアセンブラ言語を取り上げます。

私が大学4年の11月に探したソフトウエア系派遣会社の技術研修で、最初に学んだのは、IBMシステム370のアセンブラ言語だった。

次に実際の派遣先の三菱電機では、産業用コンピュータのアセンブラ言語を学び、さらにアドレス操作機能を追加したFORTRANを学んだ。

翌年の1981年夏にはS社に転職し、その厚木工場で初めてC言語を学んだ。

それ以降、C言語とアセンブラ言語を使って、ツールプログラム等の開発商品化を担当したのは前回の記事でも取り上げた。

そこで、まず、C言語の簡単な例を挙げて、その特徴を説明しよう。

分かり易い例として、C言語のサブルーチンの呼び出しを取り上げる。

なおC言語とアセンブラ言語で一番重要な点は、アドレスとプログラムとデータの区別だ。アドレスはプログラムの命令コードや変数データの位置つまり番地を示すもので、アドレスを持つ変数をポインタと呼ぶ。

/* C言語 サブルーチン呼び出し例 */
  char file1[] = "sample";
  char ext1[] = "jpg";
  char filebuf[16];

  makeFileName(file1, ext1, filebuf);

file1、ext1、filebufは、ローカル変数ではなくグローバル変数を想定する。

ローカル変数はスタック上に確保されて、サブルーチンでは使用後に破壊されるが、グローバル変数はメモリ上に残り、バグで停止した際のメモリダンプを取ればその変数内の数値を16進数で確認できる。

makeFileName()はファイル名を作るサブルーチンで、上記の例では"sample"と"jpg"を渡されてfilebufバッファに"sample.jpg"を作成する。次がそのソースコード。

/* C言語 サブルーチン本体 */
void makeFileName(char *file, char *ext, char *buf)
{
  while(*file) *buf++ = *file++; /* loop1 */
  *buf++ = '.';                  /* dot */
  while(*ext) *buf++ = *ext++;   /* loop2 */
  *buf = 0;                      /* exit */
}

3つのポインタfile、ext、bufの使い方にC言語の特徴がよく出ている。

/* loop1 */のwhile文で、ポインタfileが指す名前"sample"を最初から1文字ずつ確認して、0でなければポインタbufが指すバッファに1文字ずつコピーする。

file++、buf++の++は、ポインタのアドレス値を1ずつ増やすことを意味する。++が後につくので処理(コピー)をしてから1ずつ増やす。++が前につけば1ずつ増やしてから処理する、例 *++buf = *++file 私なら *(++buf) = *(++file) と書く。ただしこのサブルーチンでは、*buf++ = *file++ が正しいのは明らか。

呼び出し側の char file1[] = "sample"; には"sample"の後に、終端子の0が設定されているから、その0を検出してwhileループが終了する。char ext[] = "jpg"も同様。TRUE=1(0以外)、FALSE=0で、while(0)となると終了。

/* dot */では、"sample"の後に、'.'の1文字を挿入。

同じく/* loop2 */while文は、ポインタextが指す名前"jpg"を最初から1文字ずつ確認して、0でなければポインタbufが指すバッファに1文字ずつコピーする。

/* exit */で、bufの最後に終端子の0をセットする。

なお /* と */ で囲まれた文字列は、C言語の処理系ではコメントと判断される。

ツールプログラムの後は、S社のIBM PC互換機+MS-DOSの環境で、MS-DOSデバイスドライバのソフトウエア開発商品化をいくつか担当した。当時、MS-DOSのデバイスドライバはIntel8086 CPUのアセンブラ言語で書く必要があった。

ツールプログラムではZ80 8bit CPU+CP/Mの環境で、アドレスは16bitで64Kbytesのメモリだったが、Intel8086 CPUでは各種の16bitレジスタがあって、データ用のax、bx、cx、dx、アドレス用のbpとspのポインタレジスタとsi、diのインデックスレジスタに、cs、ds、es、ssのセグメントレジスタがあった。勿論、実行する命令コードを指すIP(Instruction Pointer)とF(Status Flag)レジスタも存在する


レジスタとはコンピュータのハードウエアであるCPUの一部で物理的な実体であり、アドレスやデータやステータスを保持してCPU処理の中核となる

ax、bx、cx、dxは、それぞれ8bitレジスタのah、al、bh、bl、ch、cl、dh、dlとして分けて使うことも可能。ax=ah+alでahが上位8bit、alが下位8bit。その他も同様。

アドレスはセグメントレジスタとポインタレジスタ、セグメントレジスタとインデックスレジスタの組み合わせで表現されて、合わせて20bit(4bit+16bit)アドレス。メモリで計算すると、16x64Kbytes=1024Kbytes=1Mbytesまで実装可能だった。

そこで上記のC言語のソースコードと同じ機能を持つプログラムをアセンブラ言語を使って書いてみよう。

以下がそのソースコード。

ただしgroup、segment、assume、public、ends等の記述は私の記憶とネットの情報を元に適当に加えただけで、アセンブラ言語の処理系に通した訳ではない。実際にアセンブラ言語のソースコードを処理系に通してアセンブルする際には、その処理系の仕様とサンプルソースコードを参照する必要がある。

なおアセンブラ言語の処理系では、コメントは ; で始める。

; アセンブラ言語(Intel8086) サブルーチン呼び出し例
DGROUP group dseg
;
; data segment
;
dseg segment word public 'data'
     assume  ds:DGROUP
     public  file1, ext1, filebuf
;
file1   db   'sample', 0
ext1    db   'jpg', 0
filebuf ds   16
dseg ends
;
; code segment
;
cseg segment byte public 'code'
     assume  cs:cseg, ds:DGROUP
;
     lea     ax, offset file1
     push    ax
     lea     ax, offset ext1
     push    ax
     lea     ax, offset filebuf
     push    ax
     call    makeFileName
     add     sp, 6
;
cseg ends
;
     end

サブルーチン用の3つの引数file1、ext1、filebufのアドレスをaxレジスタにロードした後に、push命令を使ってスタックにコピーして、サブルーチンに渡す。

leaはload effective addressを意味する命令コードで、実効アドレスをレジスタにロードする。

call makefFileNameは、サブルーチンmakeFileNameを呼び出す(call)命令。

add sp,6はsp(stack pointer)の値に6を加算する
。3つの引数それぞれで、2bytesずつスタックを使用したので、スタックを開放する為に3x2=6の数値を足す。


; アセンブラ言語(Intel8086) サブルーチン本体
cseg segment byte public 'code'
     assume  cs:cseg, ds:DGROUP
     public  makeFileName
;
makeFileName proc near
     push    si
     push    di
     mov     si, word ptr ss:[sp+(4+2+6)]
     mov     di, word ptr ss:[sp+(4+2+2)]
loop1:
     mov     al, byte ptr ds:[si]
     and     al, al
     jz      dot
     mov     byte ptr ds:[di], al
     inc     si
     inc     di
     jmp     loop1
dot:
     mov     byte ptr ds:[di], '.'
     inc     di
     mov     si, word ptr ss:[sp+(4+2+4)]
loop2:
     mov     al, byte ptr ds:[si]
     and     al, al
     jz      exit
     mov     byte ptr ds:[di], al
     inc     si
     inc     di
     jmp     loop2
exit:
     mov     byte ptr ds:[di], al
     pop     di
     pop     si
     ret
     end     proc
;
cseg ends

ss:、ds:はポインタとインデックスの各レジスタと対になるセグメントレジスタを明示的に書いただけで、デフォルトのセグメントレジスタと同じなので、実際には書く必要はない。なおMS-DOSのdebugコマンドで、16進機械コードを逆アセンブルした場合は、デフォルトのセグメントレジスタは表示されない。

ここでloop1:、dot:、loop2:、exit:の各ラベルから始まるアセンブル命令の集合が、C言語の/* loop1 */、/* dot */、/* loop2 */、/* exit */のコメントを付加したソースコードの各行と同じ内容の処理を行っている。

byte ptrはbyte単位でメモリにアクセスすることを意味し、word ptrはword単位でメモリにアクセスすることを意味する。勿論、1 word = 2 bytes。

アキュミュレータと呼ばれる演算専用レジスタを持つCPUでは、アキュミュレータにデータを入力するだけでFレジスタが変化する場合もあったと思うが、Intel8086のmov命令の実行ではFレジスタは変化しないので、終端子の0を検出するためにand al,alを実行する。その結果、Fレジスタのzeroフラグ(ZF)がオンになれば終端子が来たと判断し、jz(jump zero 条件ジャンプ)でループを抜ける。zeroフラグがオフであればinc siとinc diを実行してアドレスを1ずつ増やして続きを実行する為に、少し前のラベルloop1:のアドレスにjmp loop1(無条件jump)で戻りループを続ける。

ラベルloop2:以下も同じ処理方法で、exit:ラベル以下ではバッファに終端子の0を設定して具体的な処理は終了となる。

なおaxレジスタは一般に戻り値として使用されるので、サブルーチン内で破壊しても気にしないが、インデックスレジスタのsiとdiは呼び出し側の値を破壊しない為、サブルーチンの最初で、push siとpush diを実行してスタックに保存して、処理が終われば、pop diとpop siを実行してsiとdiの値を復活させる。

スタックはFirst In Last Outのメモリなので、siとdiに対するpushとpopの順番を最初と最後で逆にしている。

サブルーチン側で引数を取り出す手順も説明しよう。

サブルーチンmakeFileName()が呼び出され制御が移った時点では、スタックの内容は以下のようになっている。

ss:sp → +-----------------------------------+
      +2 |callの戻りアドレス(2bytes=16bits)  |
         +-----------------------------------+
      +4 |引数3:filebufのアドレス(2bytes)    |
         +-----------------------------------+
      +6 |引数2:ext1のアドレス(2bytes)       |
         +-----------------------------------+
      +8 |引数1:file1のアドレス(2bytes)      |
         +-----------------------------------+

ここで2つのpush命令でsiとdiレジスタをセーブすれば、スタックは以下となる。

ss:sp → +-----------------------------------+
      +2 |diレジスタセーブ値(2bytes=16bits)  |
         +-----------------------------------+
      +4 |siレジスタセーブ値(2bytes)         |
         +-----------------------------------+
      +6 |callの戻りアドレス(2bytes)         |
         +-----------------------------------+
      +8 |引数3:filebufのアドレス(2bytes)    |(4+2+2)
         +-----------------------------------+
     +10 |引数2:ext1のアドレス(2bytes)       |(4+2+4)
         +-----------------------------------+
     +12 |引数1:file1のアドレス(2bytes)      |(4+2+6)
         +-----------------------------------+

その為、引数file1やfilebufのアドレスを取得する際、分かり易いように(4+2+6)や(4+2+2)と書いたが、

  mov  si, word ptr [sp+12]
  mov  di, word ptr [sp+8]

で十分。ext1のアドレスも同様に取得する。

昔話で恐縮だが、最初にプログラマーとして働いた三菱電気の産業用コンピュータでは、アプリケーションの通常の処理ではスタックを使わなかった。その場合どのようなサブルーチンの呼び出しになるかと言えば、以下の形式で引数を渡した。

        call makeFileName
戻り→  dw   (file1のアドレスを格納)
        dw   (ext1のアドレスを格納)
        dw   (filebufのアドレスを格納)
戻り2→ (makeFileNameから戻る場合、3を加えてここにjump)

サブルーチンmakeFileName()では、特定のレジスタに戻りアドレスが渡されるので、その戻りアドレスをもとに3つの引数file1、ext1、filebufのアドレスを取得する。そして処理をした後に、retの代わりに、戻りアドレスに3を加えてjumpする。

何故、6ではなく3を加えるか、と言えば、当時の三菱電気の産業用コンピュータは、byte単位にメモリのアドレスをふったbytesマシンではなく、word(=2bytes)単位にアドレスをふったwordマシンだったから。アドレスは16bitだがwordマシンなのでメモリ容量は64Kwords=128Kbytesだった。

ただ三菱電気の産業用コンピュータでも、アセンブラ言語の二モニック表を見ると、スタックを操作する命令セットが存在したと思う。

何故なら、割り込み処理ではスタックを使ったはずだから。

一般に割り込み処理では、IPやFレジスタを含め全てのレジスタの値がスタックへ退避されて、登録した割り込み処理ルーチンに飛んでくる。

割り込み処理ルーチンでは以下のことを行う。

1. 割り込みをディセーブル(disable)にする。
2. 割り込み処理ルーチンが担当するハードウエアからのデータ入出力を行う。
3. ハードウエアの割り込み信号をオフ、必要によりハードウエアを再設定する。
4. 割り込みをイネーブル(enable)にする。
5. 割り込み処理からリターン(iret)する。

iretした後は、IPやFレジスタを含め全てのレジスタの値がスタックから復活され、割りこみで中断した途中のアドレスからプログラムの実行が再開される。

IPやFレジスタの内容を直接メモリにセーブする命令があれば、スタックを使用しなくても割り込み処理ルーチンが実装可能だろうが、スタックを使用した方が簡潔にコーディングできる。

私が三菱電気で働き始めた1980年の前半は、コーディングシートに書いたプログラムをキーパンチャーにパンチ依頼して、出来上がった紙カードの束を実機に持って行き、アセンブル、コンパイル、リンクして実機でテストする。バグがあれば自分で該当する紙カードを1枚ずつパンチし、再度、アセンブル、コンパイル、リンク、テスト。完成したオブジェクトは紙テープに出力して持ち帰る、という作業手順。

プリンタ、紙カードリーダー、紙テープ入出力装置等は、すべてアサイン文で指定する形式の
JCL(Job Control Language)の世界だった。


1980年代後半になると、紙カードではなく8inchのフロッピーディスクに出力してもらって、TSS端末でハードディスクに割り振られたプロジェクトごとの領域にコピーし、アセンブル、コンパイル、リンク。出来たオブジェクトをフロッピーディスクにコピーして実機に持って行きテスト。バグがあればTSS端末に戻り、ラインエディタを起動しプログラムのソースコードを1行単位で編集した

つまり紙カードや紙テープが、フロッピーディスクに置き換わった。

S社に転職した後は、8inchや5inchのフロッピーディスクが自社製の3.5inchのフロッピーディスクに代わった。

その後、プロッピーディスクはハードディスクに置き換わり、年月の経過とともに、Z80+CP/Mでのツールプログラムの開発商品化からMS-DOSデバイスドライバの開発商品化等を経て、次に業務用MPEGエンコーダーの開発商品化ではWindows上のDLLやデバイスドライバの作業に移った。

使用するOSもCP/M、MS-DOS、unix、Windowsへと変わっていった。エディタもunixではviだったが、それ以外はその当時のスクリーンエディタを使用。

MS-DOSデバイスドライバまでは小規模なプログラムだったので、一人で一つのソフトウエアを担当したが、Windowsからはソフトウエアの規模も大きく、チームでの作業。checkoutでファイルをダウンロードし修正してcheckinでアップロードするソースコード管理ツールも使用した。

Windows以降はデバイスドライバでもC言語だけで記述した。ただ一部にインラインアセンブラを使用したことはある。

オーディオ新商品のファームウェア開発でもチームで作業。私はチューナーのIEEE1394I/F部分を担当した。その当時、同僚のソフト屋さん達が、Dana Editorという安価なシェアウエアを使っていたので、私もマネをしてポケットマネーで購入した。そのDana Editorは今でもブログの入力用に使っている。

その後、5年程ソフトウエア開発から足を洗い、検証やユーザーサポートの仕事を担当したが、2005年3月にS社をリストラ退職した後、またプログラマーとして復帰。

その時はもう熟年で、ソフト屋として残り少ない就業期間になることが明白だったので、どんな作業形態や作業内容でもかまわなかった。

ただ入社した中小企業では、まずS社の厚木工場改め厚木テックでファームウエア開発に従事した。最初はシステム・オン・チップ形式の新拡張エンコードボード。そのチップ内のファームウェアを一人で担当。ゼロからの開発だったがENCチップの設定や自身のオブジェクトのhexファイルをフラッシュメモリに書き込む簡単な内容で特に難しい点はなかった。その拡張ボードは商品化がペンディングになったが、次に2台の業務用VTRをRS422で接続して制御するVTRコントローラーのファームウエアをこれも一人で担当した。これは既存のソースコードの修正とデバッグだったが、ファームウエア内部のモード遷移をテーブルで記述した斬新なプログラムで、面白かった。

ただ残念ながら、商品化が終了する前に派遣契約が終了した。S社の労働組合からクレームがあったようで、S社をリストラ退職した後に派遣でS社で働いている人が私以外にも多数いたらしく、問題になったとか。

元々S社のリストラはトップからの指示で、現場は人手不足。S社の経験者なら派遣でも雇いたいのが現場の本音だろう。ただ労働組合はリストラに反対なので問題視するのも理解できる。

その後は、豊洲の日本ユニシスで、証券データの加工転送プログラムのバージョンアップを担当した。これも数人のチームでの作業で、バージョンアップといっても証券データ加工の部分は新規に作り直した。unix上でのC++言語プログラミング。当然ながらエディタはviで、前回のJavaのファイル名修正プログラムでも使用したStringクラスライブラリを使用した。

ここで再びmakeFileName()サブルーチンに戻ると、C言語のwhile文を使う以外に、strcpy()、strcat()ライブラリ関数を知っていると、makeFileName()は、

void makeFileName(char *file, char *ext, char *buf)
{
  strcpy(buf, file); /* loop1 */
  strcat(buf, ".");  /* dot */
  strcat(buf, ext);  /* loop2 */
}

と書ける。ただZ80+CP/Mでツールプログラムを開発した際には、C言語標準ライブラリを使うと結構メモリを消費するので、同じ機能を持つ関数をアセンブラ言語で簡潔に記述し、標準ライブラリを使わずにメモリを節約したことが多かった。

これがC++では、もっと簡単になる。

呼び出し側は、以下。

  String file1 = "sample";
  String ext1 = "jpg";
  String filebuf;

  filebuf = makeFileName(file1, ext1);

呼び出される側は、以下。

String makeFileName(String file, String ext)
{
  return file + "." + ext;
}

もう関数やサブルーチンにする必要もない、という感じ。

記憶に残っている仕事をもう一つ紹介しよう。私が所属していた中小企業ではなく、別のシステムハウスの拠点が名古屋にあり、Densoのカーナビの操作画面の部分を担当していた。その納期が三月末だったが、二月初旬の段階でバグが収束していなかった。そこで会社をまたいで熟年プログラマーの私にまで声がかかったのだ。

名古屋への長期出張という形で、ウィークリーマンションを借りて、そのシステムハウスの拠点でデバッグを手伝った。言語はC++。拠点でソースコードを修正してコンパイルとリンクを行って、テストはDensoの工場に移動して行う。バグが多い上に新たなバグも発生していて、週一回休める程度のハードワークだったが、私の猫の手の助けもあって、何とか三月末にはバグが収束して間に合った。

ここで使っていたのがフリーウエアのYokka。エディタのプログラム名はNoEditorで、一緒にインストールされるYokkaGrepが秀逸。unix上でプログラム開発をした経験があれば、viエディタとccコンパイラに加えて、ファイル比較のgrepコマンドは頻繁に使用したはずだ。

YokkaGrepは、フォルダを指定してそのファルダ内に含まれるファイルを全て比較できる。ソフトウエア開発のプロジェクトでは各バージョンのソースコード一式をサーバー上のそれぞれのフォルダに入れて保存している。だからcheckoutしてフォルダ単位でパソコンにダウンロードして、YokkaGrepでフォルダごと比較すれば、一度の操作で全ファイルの差異を表示できる。

Yokkaは今でも窓の杜からダウンロード可能。ファイル名変更のJavaプログラムを書いた時にも、Yokkaに含まれるNoEditorを使った。

その後、2010年春には引退して、リタイヤ生活に入った。

過去を振り返って、果たして私にはプログラミングの才能があったのだろうか?

個人的にはプログラミングの才能はなかったと考える。何故なら、プログラミングしていても天才的な閃きはないし、新奇なアイデアが浮かぶ訳でもない。記憶力が悪いので、常に資料を参照したり今までのソースコードを読み直して、一歩一歩、着実に前に進む。ただそれだと時間がかかるので、頭の回転を速くする必要がある。

なおそれはプログラミング以外にも一般的に当てはまる昔から自明の事実で、その為の努力は既に中学生の頃から始めていた。

物理学専攻の大学生だった頃には、コンピュータを見たことも触ったこともなく、ソフトウエアの知識も全くなかった。だから就職後は独学で勉強して、早期に仕事が出来るまでに自分の能力を立ち上げ、一年少々の経験でもS社に転職できた。

就職した年の1980年冬、横浜市に新設された下水処理場のコンピュータシステムの現地調整に加わったことがあった。その終わり頃の飲み会からの帰りに、現場での上司にあたる三菱電機の社員と帰宅する方向が一緒で、雑談しながら帰ったのだが、私が酔った勢いで「もっと勉強して早く今の派遣会社から逃げ出すんだ」と言うと、三菱の社員は「いや逃がさん」と言っていた。実際、翌年の夏に、私はその会社を逃げ出してS社に転職したのだった。

勿論、プログラミングの仕事やプログラマーの求人が多い時代で、私が若い頃には比較的景気がよかったことも幸運だった。

さらに言えるとしたら、自分自身をよく理解して自分の性格にあった仕事を選び、自分や周囲の現状を分析して最適な方法を選択する必要がある、ということだろう。