Open Watcom IDE -- Cからとアセンブラールーチンを呼び出す2018年03月02日 17:34

Open Watcom IDEをいじるに当たり、ただ漠といじってもつまらないので、 PC-DOS用のシェルの作成を計画しました。もちろんネタ本があります。 CQ出版社から1989年7月20日に出版された「Allen Holib著 横山和由訳 MS-DOS用Shellの実現 (On Command: Writing a Unix-Like Shell for MS-DOS)」です。 本棚の肥やしになっていたのを引っ張り出してきました。この本を購入した頃は、 まだまだ、コンパイラーなどのツールが高価で、実際の制作には、踏み切れませんでした。

内容は、丁寧にシェルの実装について、ソースリストを交えて解説してあります。 特に難しい内容ではなく、容易に著者の開発手順をたどり、シェルを作成することができそうです。 原著が書かれたのは、1986年頃で、その当時書き方で、MS-C Ver.4とMASMで記述されています。 現代のコーディングスタイルと比較して、古い書き方をしてあり、私の好みに合わない書き方です。 今回は、現代風のスタイルに合わせて書いていきます。主な変更点は、以下の通りです。

  1. 関数の宣言で、"int"型の変数を明示的に宣言していないのを 全て宣言するようにした。
  2. "unsinged int"型の宣言を "unsinged"としてあるのを、"unsinged int"と記述するようにした。
  3. register変数は、とりあえず使わない。
  4. ユーティリティーライブラリーは複数のソースファイルに分かれているが、共通の インクルードファイルにまとめてプロトタイプ宣言をしている。デバックしづらいので、一つの Cソースファイルに対応して一つのヘッダーファイルを作成しプロトタイプ宣言をするようにした。
  5. 標準関数のプロトタイプ宣言を 直接コーディングしてあるのを ヘッダファイルをインクルード するようにした。
  6. Static属性の付いた関数は、そのままではデバッグしづらいので、コンパイル時に"DEBUG"を定義してないときは、 static属性をつけてコンパイルをするように"#ifndef DEBUG"を関数の宣言に追加した。
  7. 制作にはOpen Watcom C/C++コンパイラーとWASMを使用する。
  8. 開発はOS/2で行い、デバッグはOS/2のPC-DOS窓で行う。

開発はC:ドライブに"Shell"ディレクトリを作成し、その中にコードを書くこととしました。

前準備ができたので、まずは、手始めに、Cのmain()関数から、 アセンブラーで記述した関数foo()を呼び出す練習をしました。 ソースコードは、下記の通りです。

          /* callasm.c */
          #include 
          #include "foo.h"

          int main() {
             int p;

             p = 7;
             printf("%d => %d\n",p,foo(p));
             return 0;
          }
          /* foo.h */
          extern int foo(int);
          ; foo.asm
          _text    SEGMENT BYTE PUBLIC 'CODE'
          _text    ENDS
          _data    SEGMENT WORD PUBLIC 'DATA'
          _data    ENDS
          const    SEGMENT WORD PUBLIC 'CONST'
          const    ENDS
          _bss     SEGMENT WORD PUBLIC 'BSS'
          _bss     ENDS
          dgroup   GROUP const,_bss,_DATA
                   ASSUME  CS:_text, DS:dgroup, SS:dgroup, ES:dgroup
          _text    SEGMENT
          ; ---------------------------------------------------------------------
          ;        foo(int val)
          ;        val=bp+4
          _foo     PROC NEAR
                   PUBLIC _foo
                   push bp
                   mov bp,sp
                   mov ax,[bp+4]
                   shl ax,1
                   mov sp,bp
                   pop bp
                   ret
          _foo     ENDP
          _TEXT    ENDS
                   END

Wasmは、MASM互換といわれているので、一発でうまくいくと思っていたのですが、だめでした。 ビルドしてみると、下記のようにリンケージでエラーが出ます。callasm.cで呼び出している関数foo()は、 コンパイルされると、"foo_"となるようです。

          cd C:\SHELL\SAMPLE
          wmake -f C:\SHELL\SAMPLE\callasm.mk -h -e C:\SHELL\SAMPLE\callasm.exe
          wasm FOO.ASM -i="E:\WATCOM/h" -ms -fp0 -w4 -e25
          Open Watcom Assembler Version 1.9
          Portions Copyright (c) 1992-2002 Sybase, Inc. All Rights Reserved.
          Source code is available under the Sybase Open Watcom Public License.
          See http://www.openwatcom.org/ for details.
          FOO.ASM: 27 lines, 0 warnings, 0 errors
          wcc callasm.c -i="E:\WATCOM/h" -w4 -e25 -zp2 -od -fp2 -fpi87 -bt=dos -fo=.obj -ms
          Open Watcom C16 Optimizing Compiler Version 1.9
          Portions Copyright (c) 1984-2002 Sybase, Inc. All Rights Reserved.
          Source code is available under the Sybase Open Watcom Public License.
          See http://www.openwatcom.org/ for details.
          callasm.c: 11 lines, included 773, 0 warnings, 0 errors
          Code size: 60
          wlink name callasm d all sys dos op m op maxe=25 op q op symf @callasm.lk1
          Error! E2028: foo_ is an undefined reference
          file callasm.obj(C:\SHELL\SAMPLE\callasm.c): undefined symbol foo_
          Error(E42): Last command making (C:\SHELL\SAMPLE\callasm.exe) returned a bad status
          Error(E02): Make execution terminated
          Execution complete
というわけで、foo.asm内の"_foo"を"foo_"に編集し再ビルドしました。
          ; foo.asm
          _text    SEGMENT BYTE PUBLIC 'CODE'
          _text    ENDS
          _data    SEGMENT WORD PUBLIC 'DATA'
          _data    ENDS
          const    SEGMENT WORD PUBLIC 'CONST'
          const    ENDS
          _bss     SEGMENT WORD PUBLIC 'BSS'
          _bss     ENDS
          dgroup   GROUP const,_bss,_DATA
                   ASSUME  CS:_text, DS:dgroup, SS:dgroup, ES:dgroup
          _text    SEGMENT
          ; ---------------------------------------------------------------------
          ;        foo(int val)
          ;        val=bp+4
          foo_     PROC NEAR
                   PUBLIC foo_
                   push bp
                   mov bp,sp
                   mov ax,[bp+4]
                   shl ax,1
                   mov sp,bp
                   pop bp
                   ret
          foo_     ENDP
          _TEXT    ENDS
                   END

再ビルドしてみると、エラーはなくなりました。

          cd C:\SHELL\SAMPLE
          wmake -f C:\SHELL\SAMPLE\callasm.mk -h -e C:\SHELL\SAMPLE\callasm.exe
          wasm FOO.ASM -i="E:\WATCOM/h" -ms -fp0 -w4 -e25
          Open Watcom Assembler Version 1.9
          Portions Copyright (c) 1992-2002 Sybase, Inc. All Rights Reserved.
          Source code is available under the Sybase Open Watcom Public License.
          See http://www.openwatcom.org/ for details.
          FOO.ASM: 27 lines, 0 warnings, 0 errors
          wlink name callasm d all sys dos op m op maxe=25 op q op symf @callasm.lk1
          Execution complete

テストです。

          C:\SHELL\SAMPLE>callasm
          7 => 3522

アセンブラールーチン"foo"は、受け取った引数を左に1bitシフトする事で受け取った引数を2倍して値を返します。 結果は、"14"になるはずですが、そうなっていません。確認のため、 "callasm.c"のコンパイル結果を見るためにコンパイルリストをコンパイラーに吐かせようとしましたが、"wcc"のオプションに 該当するものが見当たらいません。調べてみると"Open Watcom Assembler"Wikiの"Disassembler"の項目に、 以下の記述を見つけました。

          The assembler does not have listing facilities; instead the use of
          wdis for generating listings is recommended.

この指示に従い、callasm.objを"wdis"で、逆アセンブルしてみました。

          Module: C:\SHELL\SAMPLE\callasm.c
          GROUP: 'DGROUP' CONST,CONST2,_DATA

          Segment: _TEXT BYTE USE16 0000003C bytes
          0000                          main_:
          0000    B8 18 00                  mov         ax,0x0018 
          0003    E8 00 00                  call        __STK 
          0006    53                        push        bx 
          0007    51                        push        cx 
          0008    52                        push        dx 
          0009    56                        push        si 
          000A    57                        push        di 
          000B    55                        push        bp 
          000C    89 E5                     mov         bp,sp 
          000E    81 EC 04 00               sub         sp,0x0004 
          0012    C7 46 FE 07 00            mov         word ptr -0x2[bp],0x0007 
          0017    8B 46 FE                  mov         ax,word ptr -0x2[bp] 
          001A    E8 00 00                  call        foo_ 
          001D    50                        push        ax 
          001E    FF 76 FE                  push        word ptr -0x2[bp] 
          0021    B8 00 00                  mov         ax,offset DGROUP:L$1 
          0024    50                        push        ax 
          0025    E8 00 00                  call        printf_ 
          0028    83 C4 06                  add         sp,0x0006 
          002B    C7 46 FC 00 00            mov         word ptr -0x4[bp],0x0000 
          0030    8B 46 FC                  mov         ax,word ptr -0x4[bp] 
          0033    89 EC                     mov         sp,bp 
          0035    5D                        pop         bp 
          0036    5F                        pop         di 
          0037    5E                        pop         si 
          0038    5A                        pop         dx 
          0039    59                        pop         cx 
          003A    5B                        pop         bx 
          003B    C3                        ret         

          Routine Size: 60 bytes,    Routine Base: _TEXT + 0000

          No disassembly errors

          Segment: CONST WORD USE16 0000000A bytes
          0000                          L$1:
          0000    25 64 20 3D 3E 20 25 64 0A 00                   %d => %d..

          Segment: CONST2 WORD USE16 00000000 bytes

          Segment: _DATA WORD USE16 00000000 bytes

これを見ると、"0017"行目から"001A"行目で、"ax"レジスタに引数の値を入れて、 "foo_"を呼び出しているのが分かります。想定外でした。関数の引数は、基本スタックに 積む物だと思っていたのですが、Open Watcom C/C++では違うようです。困りました。アセンブラールーチンを 書き換えたくはありませんので、コンパイラーのオプションスイッチで、切り抜ける方法を探ってみたところ、 該当しそうなオプションスイッチがありました。

          -ecc          set default calling convention to __cdecl
          -ecd          set default calling convention to __stdcall
          -ecf          set default calling convention to __fastcall
          -ecp          set default calling convention to __pascal
          -ecr          set default calling convention to __fortran
          -ecs          set default calling convention to __syscall
          -ecw          set default calling convention to __watcall (default)

OS/2のシステム関数を呼び出すときには、関数に"CDECL"とつけていたように記憶しています。"-ecc"が 怪しいです。まずは、foo.asm中の"foo_"を"_foo"に戻し、"-ecc"をつけて再ビルドしてみました。

          cd C:\SHELL\SAMPLE
          wmake -f C:\SHELL\SAMPLE\callasm.mk -h -e C:\SHELL\SAMPLE\callasm.exe
          wasm FOO.ASM -i="E:\WATCOM/h" -ms -fp0 -w4 -e25
          Open Watcom Assembler Version 1.9
          Portions Copyright (c) 1992-2002 Sybase, Inc. All Rights Reserved.
          Source code is available under the Sybase Open Watcom Public License.
          See http://www.openwatcom.org/ for details.
          FOO.ASM: 27 lines, 0 warnings, 0 errors
          wcc callasm.c -i="E:\WATCOM/h" -w4 -e25 -zp2 -ecc -od -fp2 -fpi87 -bt=dos -fo=.obj -ms
          Open Watcom C16 Optimizing Compiler Version 1.9
          Portions Copyright (c) 1984-2002 Sybase, Inc. All Rights Reserved.
          Source code is available under the Sybase Open Watcom Public License.
          See http://www.openwatcom.org/ for details.
          callasm.c: 11 lines, included 773, 0 warnings, 0 errors
          Code size: 63
          wlink name callasm d all sys dos op m op maxe=25 op q op symf @callasm.lk1
          Execution complete

callasm.objを逆アセンブルした結果は、以下のようになり、関数のパラメーターはスタックに積んで 渡すようになりました。

Module: C:\SHELL\SAMPLE\callasm.c
GROUP: 'DGROUP' CONST,CONST2,_DATA

Segment: _TEXT BYTE USE16 0000003F bytes
0000                          main_:
0000    B8 18 00                  mov         ax,0x0018 
0003    E8 00 00                  call        __STK 
0006    53                        push        bx 
0007    51                        push        cx 
0008    52                        push        dx 
0009    56                        push        si 
000A    57                        push        di 
000B    55                        push        bp 
000C    89 E5                     mov         bp,sp 
000E    81 EC 04 00               sub         sp,0x0004 
0012    C7 46 FE 07 00            mov         word ptr -0x2[bp],0x0007 
0017    FF 76 FE                  push        word ptr -0x2[bp] 
001A    E8 00 00                  call        _foo 
001D    83 C4 02                  add         sp,0x0002 
0020    50                        push        ax 
0021    FF 76 FE                  push        word ptr -0x2[bp] 
0024    B8 00 00                  mov         ax,offset DGROUP:L$1 
0027    50                        push        ax 
0028    E8 00 00                  call        printf_ 
002B    83 C4 06                  add         sp,0x0006 
002E    C7 46 FC 00 00            mov         word ptr -0x4[bp],0x0000 
0033    8B 46 FC                  mov         ax,word ptr -0x4[bp] 
0036    89 EC                     mov         sp,bp 
0038    5D                        pop         bp 
0039    5F                        pop         di 
003A    5E                        pop         si 
003B    5A                        pop         dx 
003C    59                        pop         cx 
003D    5B                        pop         bx 
003E    C3                        ret         

Routine Size: 63 bytes,    Routine Base: _TEXT + 0000

No disassembly errors

Segment: CONST WORD USE16 0000000A bytes
0000                          L$1:
0000    25 64 20 3D 3E 20 25 64 0A 00                   %d => %d..

Segment: CONST2 WORD USE16 00000000 bytes

Segment: _DATA WORD USE16 00000000 bytes

テストです。

          C:\SHELL\SAMPLE>callasm
          7 => 14

成功です。これで、Cからアセンブラールーチンを呼び出す手順を確認できました。一山、越えました。

IDEその後2018年03月26日 15:58

Open WATCOM C/C++のIDEを使って、前回説明したルールに沿って PC-DOS SHELLの製作を続けています。

幾つかサンプルを示します。

今回、製作するプロジェクト名は、"SHELL"とします。作業デイレクトリーは"C:\SHELL"です。 また、プロジェクトのターゲットは、3つです。完成品"SH.EXE"、デバッグ用ターゲット"DEBMAIN.EXE"。 いずれも、DOS 16ビットの実行モジュールです。 この外に、テキストでは、ライブラリー"SHELLIB.LIB"を作りますので、 これもターゲットとします。

さて、さきに決めたコーデイングルールに従い、プリグラムを書いていきます。幾つかサンプルを示します。

関数の頭書きの例です。オリジナルはこのようになっています。

   /* cpy.c */
   char *cpy(dest,src)
   register char *dest,*src;
   {
         :
       (中略)
         :
       return dest;
   }
これ下記のようにすると同時にヘッダーファイルを追加します。
   /* cpy.c */
   char *cpy(char *dest,char *src) {
         :
       (中略)
         :
       return dest;
   }

ヘッダーファイルは以下のとおりです。これを必要に応じてインクルードします。

   /* cpy.h */
   extern char *cpy(char*,char*);

型を省略された引数は、きちんと宣言します。例えば、

   /* next.c */
   char *next(**linep,delim,esc) {
   char **linep;
   {
         :
       (中略)
         :
      return start;
   }

これは、このようにします。もちろん、ヘッダーファイルも同様です。

   /* next.c */
   char *next(char **linep,char delim,char esc) {
   {
         :
       (中略)
         :
      return start;
   }
   /* next.h */
   extern char *next(char **,char,char);

標準関数の定義は、本書では以下のようにしていますが、 ヘッダーファイルをインクルードするようにします。下記の例には、strlen()の宣言に誤植があります。 引数が一つなのにカンマが付いています。おそらく、版下を作成するのに、ソースファイルを 手書きで入力したのでしょう。現代ならば、インクルードして組み込むでしょうね。

   /* strsave.c */
   extern int  strcpy (char*,   char*);
   extern char *malloc(unsigned      );
   extern int  strlen (char*,        );
   
   char *strsave(str)
   char *str;
   {
         :
       (中略)
         :
      return NULL;
   }

これは、下記のように記述しました。

   /* strsave.c */
   #include 
   #include 
   
   char *strsave(char *str) {
         :
       (中略)
         :
      return NULL;
   }

static宣言された関数は、そのままでは、別のコンパイル単位からは見えなくなります。 これでは、デバッグをしずらいので、コンパイル時に文字列"DEBUG"が定義されている時は、 static宣言をはずすようにします。例えば下記の場合は、

   /* reargv.c */
         :
       (中略)
         :
   extern unsigned char _osmajor;
   static int Envlen;
            :
          (中略)
            :
   /* --------------------------------------------------------------------- */
   static char *nextarg(pp)
   char **pp;
   {
            :
          (中略)
            :
      return start;
   }
   /* --------------------------------------------------------------------- */
   int reargv(argcp,argvp)
   char *(**argvp);
   int  *(  argcp);
   {
            :
          (中略)
            :
      return 1;
   }
   /* --------------------------------------------------------------------- */
   int envlen(void) {
            :
          (中略)
            :
      return Envlen;
   }
このようにします。
   /* reargv.c */
         :
       (中略)
         :
   extern unsigned char _osmajor;
   static int Envlen;
            :
          (中略)
            :
   /* --------------------------------------------------------------------- */
   #ifndef DEBUG
   static
   #endif
   char *nextarg(char **pp) {
            :
          (中略)
            :
      return start;
   }
   /* --------------------------------------------------------------------- */
   int reargv(int *argcp,char *(**argvp)) {
            :
          (中略)
            :
      return 1;
   }
   /* --------------------------------------------------------------------- */
   int envlen(void) {
            :
          (中略)
            :
      return Envlen;
   }

リスト中、MS-C依存の変数"_osmajor"があります。幸いなことに、Open WATCOM C/C++でも、 サポートされています。

"DEBUG"の宣言は、IDEの"C Compiler Switches"の "Macro Definitions: [-d]"欄に"DEBUG"と書きコンパイルします。デバッグが終わったら、 "DEBUG"を取り去り、再コンパイルしときます。

MS-Cに依存している個所は、もう一つありました。アセンブラーで一部書かれています。 その中で、MS-C独自の内部関数"__chkstk"関数を呼び出しています。これは、 実行時にスタックオーバーフローの検出をするルーチンです。Open WATCOM C/C++にはありません。 "__STK"が、同等の機能を果たすものと思われますが、仕様が公開されていません。今回は、 "__chkstk"を無視する事とします。

このテキスト中、"isdir.c"は、コードの最後の数行が欠落しています。編集ミスだと思います。 しかし、コードのコメントを読めば欠落している部分を補うことができます。 この外にも、バグを見つけました。それは、次回に報告します。