5.2 32位的DOS:控制台应用程序

5.2.1控制台应用程序的模块入口代码

  控制台应用程序的模块入口代码较普通的.EXE略有不同:

Project2. dpr.8: begin
0040823855           push ebp
004082398BECmov        ebp, esp
0040823B 83C4F0       add esp,-$10
0040823E A1B0934000     mov eax,[$004093b0]//Pointer of IsConsole
00408243 C60001        mov byte ptr [ eax],$01 //Isconsole:=True
00408246 B8F8814000     mov eax,$004081f8
0040824B E844C7FFFE      call eInitExe

  这里加入了设置系统全局变量IsConsole的代码。如果有下列条件之一,则编译器生成上述的入口代码,使内部例程_InitExe()被调用之前IsConsole为真:

  • 源代码中加入编译条件[SAPPTYPE CONSOLE};
  • 编译命令行上加入-cc参数;
  • 在IDE的项目配置中,选中Generate console application选项。

  程序所在的控制台窗口,在模块入口代码被执行之前,就已经被操作系统打开了。更详细地讲:

  • 如果父进程有自己的控制台,程序就继承这个控制台;
  • 否则,操作系统为程序创建一个控制台窗口。

  Delphi的内核只是使用这个窗口而已,并不负责它的开启。操作系统判断对于一个,EXE程序是否需要开启控制台窗口的依据,是该程序模块的PE头结构中是否指定了要使用CONSOLE系统。这一点,在下一章的“Delphi的积木艺术(PE)”中详细地讲述。
  除了在入口代码和PE头结构中进行的细微差异,一个控制台应用程序与GUI模式的.EXE并没有不同:它们的初始化和结束化是完全一致的。在Delphi内核中,只有procedure WriteErrorMessage()这一例程需要检测IsConsole变量,以决定当系统出错时,是在控制台上显示错误,还是弹出错误对话框。

 

5.2.2控制台应用程序的最小化实现

  编译器与操作系统为控制台应用程序的执行准备好了一切,接下来只须接管控制台的输入输出即可。这包括:
  使用操作系统AP1:CetStdHand1e()获取控制台的句柄;使用操作系统APl:ReadFile()和WriteFile()操作控制台的输入输出。
  下面的示例演示了控制台输出:

program MiniConsole;
(SAPPTYPE CONSOLE}

function WriteFile(hFile: Integer; 
const Buffer;nNumberofBytesToWrite: Cardinal;
var lpNumberofByteswritten: Cardinal;
lpoverlapped: Pointer): Integer; stdcal1;

external kerne132 name ' Writerile';
function GetstdHandle(nStdHandle: Integer): Integer; stdcal1;
external kerne132 name ' GetstdHandle';
const
    STD_OUTPUT_HANDLE=Integer(-11);
var
    s: ShortString=his is a Console Application.;
    HStdout: integer;
    Dummy: Cardinal;
begin
    Dummy:=Byte(S[0]);
    HStdout:=GetStdHandle(STD_OUTPUT_HANDLE);
    WriteFile(hstdout,S[1],Dummy,Dummy,nil);
end.

  要使用最小化内核编译这个程序,只须在System.pas中加入全局变量“IsConsole”的定义即可。


5.2.3控制台应用程序的Delphi实现

  下面的应用能够在最小化内核中编译出可执行目标代码:

program TestMini;
const
    MaxAsc=26;
var
    i: Integer;
    s: string[ MaxAsc];
begin
    s[0]:=chr(MaxAsc);
    fori:=1 to MaxAsc do
    byte(s[i]):=ord('a')-1+i;
    //Writeln(s);
end.

  其中,ord()和chr()两个函数是编译器内置的,而byte()是强制类型转换。因此,系统并不需要请求更多的代码(通常在标准Pascal中声明过的函数都被编译器内置)。但是,如果要使用Writeln(O函数向控制台输出,那么要同时在最小化内核中添加的内容包括:

function _WriteBytes(var t:TTextRec;const b;cnt:Longint):Pointer;
function _WriteSpaces(var t:TTextRec;cnt:Longint):Pointer;
function _WriteString(var t:TTextRec;const s:ShortString;width:Longint):Pointer;
function WriteoString (var t:TTextRec;const s:Shortstring):Pointer;
function _WriteLn(var t:TTextRec):Pointer;
function _RewritText(var t:TTextRec):Integer;
function _Close(var t:TTextRec):Integer;
function _Flush(var t:TTextRec):Integer;
procedure __roTest;
function OpenText(var t:TTextRec;Mode:Word):Integer;
function TryOpenForOutput(var t:TTextRec):Boolean;
procedure Set InOutRes(NewValue:Integer);

  其中包括文本文件操作、10检查以及与Writeln()相关的一些内部函数。此外,些相关的系统常量与变量也需要包括进来。为了使Writeln()一行代码得以编译通过,大概需要在Systerm.,pas 中加入22K的源代码!如果需要使用Read1n()等例程来操作控制台,那么需要添加的函数将会更多。
  在System.pas中与控制台相关的代码主要包括:

  • 将操作系统提供的控制台句柄以文本文件的形式描述为基本输入、输出系统:
  • 文本文件操作例程;
  • 不同入口参数的writeln()和Readln()例程;
  • IO测试。

 

5.2.4文件操作例程与控制台应用程序
  在Delphi中,少有三种方法可以操作文件,如下所示:

  • 定义File文件类型变量,使用Pascal基本例程操作文件;
  • 定义文件句柄变量,使用Win32API操作文件:
  • 定义流对象变量,使用基于流的方式操作文件。

  Delphi在Systerm,pas中实现了文件类型(File)以及文本文件类型(Text),一方面使得Delphi能够更全面地兼容Pascal的语言特征,另一方面也使得在开发人员看来,控制台应用的接口与DOS程序有更多的相似之处。在这样的相似性背后,需要明确的是:

  • 控制台是一个真实的窗应用程序,操作系统给控制台提供的资源(APl、动态链接库、CPU模式以及内存等)与其他的图形界面(GUl)应用程序并没有本质的不同。
  • 控制台应用程序是一个真正的Win32程序“,它只是在虚拟DOS的窗中运行而已。除了界面上的相似,该窗口不具有DOS操作系统的任何特征。
  • 任何进程都可以开启自己的控制台窗口。这并不需要调用CMD.EXE来实现,操作系统提供了足够的APl使程序开启并控制白己的控制台。

  Delphi使用Text文件类型隐藏了这一切细节,使得控制台程序更像一个DOSBOX,而Delphi做的,只不过是Writeln()或者Read1n()。
  这一切只是表象而已。
  Text文件类型实际上是通过TTextRec类型记录来实现的。这个记录定义了四个重要的域:

(* must match the size the compiler generates:460 bytes*)
TTextRec =packed record
OpenFunc: Pointer;
InOutFunc: Pointer;
FlushFunc: Pointer;
CloseFunc: Pointer;
//.
end;

  这些指针指明了该文件使用哪些内部例程来操作。对于Text文件记录来说,这些域都是开放的,因此可以修改这些域,使用其他方式读写文本文件一—如果能确定这些新的例程的安全性的话。
  编译器使文本文件打开例程指向下面三个函数之一,进而调用到例程OpenText():

/Wprocedure Reset(var F [: File; RecSize: Word ]);
function _ResetText(var t: TTextRec): Integer;
begin
Result:=Openrext(t, fmInput);
end;
//procedure Rewrite(var F: File [; Recsize: Word ]);
function._RewritText(var t: TTextRec): Integer;
begin
Result:=OpenText(t, fmoutput);
end;
//procedure Append(var F: Text);
function Append(var t: TTextRec): Integer;
begin
Result:=OpenText(t, fmrnout);
end;

  OpenText()初始化文本文件记录的域OpenFunc,使其指向内部例程TextOpen()。然后调用之。在TextOpen()例程中,则进一步地记录其他域,使其余三个指针分别指向内部例程:

//TnoutFunc读/写请求,指向如下例程之一
function TextIn(var t:TTextRec):Integer;
function Textout(var t:TTextRec):Integer;
//对控制台,FlushFunc指向例程FileNoPProc();对普通文件指向Textout()
function FileNOPProc(var t):Integer;
function Textout(var t:TTextRec):Integer;
//CloseFunc指向例程TextClose()
function TextClose(var t:TTextRec):Integer;

  函数TextOpenC)会根据文本文件记录的Name和Mode域来判定打开什么文件以及用什么方式打开。同时,也据此判定是操作物理文件还是控制台。其具体的规则如表5-2所示。

  如果操作对象是文件,则以OpenMode参数调用API:CreateFileA()来开启文件并将文件句柄填入Handle域;如果操作对象是控制台,则调用API:GetStdHand1e()来取得控制台的句柄并填入Handle域。
  接下来,控制台与文本文件的操作就完全一致了:TextIn()和Textout()例程只是调用APl:ReadFile()和WriteFile()而已,并不区分操作对象是控制台还是文本文件。
  至于使用Readln()与Writeln()来操作控制台或文件,则不过是Delphi用compiler magic以及字符串处理的一些技巧而已,与控制台的实现机制已经无关了。

 

5.2.5控制台的开启与关闭

  Delphi为控制台应用程序所做的最后一件事,仅仅是定义了三个变量,并在System.pas单元初始化和结束化的时候,负责开启与关闭它而已。

Input:Text;{standard input)
Output:Text;{ standard output}
ErrOutput:Text;{ standard error output }

  Delphi在初始化时并不强行初始化控制台系统,它只是将上面三个变量的Mode域置为fmClosed。在Write()和Read()例程的内部(准确地说,是_WriteBytes()和_ReadChar()例程的内部),总是会尝试打开一个关闭的文件记录:如果记录是控制台,则试图调用打开文本文件的例程;如果是文件,则返回运行期错误。
  从这里可以看到,虽然Delphi为每个模块都初始化了控制台变量,但是直到调用Write)或Read(),或者在显式调用Reset(Input)和Rewrite(Output)来打开控制台之前,这些变量的Mode都是fmClosed。也就是说,即使变量IsConsole为True,操作系统也为该应用创建了控制台窗口,但是Delphi中的这几个控制台全局变量却仍然是无效的。下面的例子证明了这一点:

  

Drogram LostConsole;
{SAPPTYPE CONSOLE}
uses
    Dialogs,SysUtils;
begin
    ShowMessage('IsConsole:'+BoolTostr (IsConsole,True));
    //显示为fmClosed
    ShowMessage('INPUT.Mode:$'+IntToHex(TTextRec(INPUT).mode,4));
    //显示为无效句柄
    ShowMessage('INPUT.Handle:$'+IntToHex(TTextRec(INPUT).Handle,4));
end.

  Delphi这样做,可以避免在.DLL、.BPL和GUI应用程序中编译进OpenText()及相关例程的代码。基于同样的原因,内核中退出时显示出错信息的例程是这样写的:

procedure WriteErrorMessage;
var
    Dummy:Cardinal;
begin
    if IsConsole then
    begin
        with TTextRec(Output)do
        begin
            if(Mode=fmoutput)and (BufPos>0)then
            TText IOFunc(InoutFunc)(TTextRec(Output));//flush out text buffer
        end;
        WriteFile(GetStdHandle(STD_OUTPUT_HANDLE),runErrMsg,
        sizeof(runErrMsg),Dummy,nil);
        WriteFile(GetstdHandle(STD_OUTPUT_HANDLE),sLineBreak,2,Dummy,ni1);
    end;
    else if not NoErrMsg then
        MessageBox(0,runErrMsg,errcaption,0);
end;

这里没有直接使用Writeln(),而是使用CetStdHand1e()例程来取得控制台句柄并输出,从而使内核不必总是编译进Writeln()及相关的代码。这些措施,可以使目标文件内核减小大约2K。


5.2.6CRT单元与Input、Output的重载

  Delphi初始时Input和Output变量是无效的,对普通开发者来说是一件微不足道的事。
毕竟绝大多数开发者都直接使用Writeln()和Read1n()。但是,这给CRT单元的开发者带来了意想不到的收获。
  CRT单元由来已久,甚至在Borland Pascal for Windows以及Delphi1.0的时代都还有WinCRT单元来模拟它。这个单元在Turbo Pascal中用于操作DOS屏幕上的光标位置、字符颜色以及键盘事件等。在DOS时代,还有大量基于CRT单元的第三方开发包,用来实现菜单、对话框等字符模式的用户界面。
  从Delphi2.0开始,就再也没有官方的CRT单元了。不过,仍然可以找到一些好用的替代品。例如:

  • CRT32:Attila Szomor 基于Frank Zimmer的CRT32.TXT编写。
  • Crt replacement for Delphi 1.20:Will DeWit Jr.编写,以下简称CrtRep。

  CRT32编写的时间较早,除实现了CRT的全部功能之外,还加入了鼠标支持等功能,但核心部分有些不稳定,而CrtRep是新近才发布在Borland Code Central上的,对文本模式支持得非常好,而且代码流畅。
以后者为例。由于Input和Output 需要在例程Write()和Read()被调用时才进行初始化。而且被初始化的只是一个入口,即TTextRec.OpenFunc指针,其他的操作例程指针,是由OpenFunc负责设置的。因此,CrtRep实现时,把自己的_CrtOpen()例程指针填入OpenFunc域,剩下的工作,就全部由_Crtopen()接管了。

  _CrtOpen()中,CrtRep按照如下的规则替换了Output记录的其他三个域(Input类同):    

f.InOutFunc:=@_CrtOut;
f.FlushFunc:=@_Crtout;
f.closeFunc:=@_Crtoutclose;

  这些例程则按照DOS的CRT单元的规则处理屏幕输出。其中,输出例程_CrtOut()是使用API:WriteConsole0utput()来实现输出的,而不是System.pas中使用的API:WriteFile(),因此才使CrtRep拥有了更强的操作控制台窗体的能力。
  Delphi缺省Input和Output无效,也给CrtRep带来一些小的麻烦。例如CrtRep不得不在单元初始化时,显式地调用Reset(Input)和Rewrite(Output)来使这两个变量有效。
  因为在普通的Delphi控制台程序中,可能发生的操作通常就只是Write()或Read(),而在使用CRT单元的控制台程序中,大多会以类似这样的方法来输出:

begin
    C1rScr;//清屏
    GotoxY(1030);//将光标定位到(10,30)位置
    Writeln(测试CRT单元。);
end.

  若没有预先使Input和Output有效,那么Writeln()前的两行代码不会产生任何效果。

转载于:https://www.cnblogs.com/YiShen/p/9887746.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值