?? 附錄a-c.txt
字號:
附錄A
控制臺程序
在Windows 操作系統中運行一個應用程序后,我們經常會看到兩種界面,一種是標準的
窗口界面,窗口界面的程序架構在第4 章中已經有了詳細的介紹;另一種是類似于MS-DOS
程序的文本界面,如常用的Ping、Xcopy 等命令使用的都是這種界面,這種界面就叫做控制
臺(Console),由于控制臺在Windows 系統中還是以一個文本窗口的方式出現的,所以一般
將這個窗口稱為控制臺窗口。
從表面看,32 位的控制臺程序和16 位的MS-DOS 應用程序在外觀和表現上都是很相似
的,比如它們都是在一個黑洞洞的文本窗口中顯示文本,都支持命令行下的重定向操作,讀
取鍵盤的方式也是一樣的。但是,在這個表象下面,兩者卻是完全不同的,DOS 應用程序是
16 位的實模式程序,而Windows 下的控制臺程序卻是不折不扣的32 位保護模式程序,它可
以使用Win32 API 函數,文件頭中同樣有導入表和導出表,可以在程序中建立多個線程執行。
總之,控制臺程序是長著“DOS 程序面孔”的Win32 程序,可以使用Win32 編程中的所有特
征。
進一步來說,如果一定要讓控制臺程序有一個窗口的話,也可以在其中使用CreateWindow
函數來創建一個窗口,這樣控制臺程序可以在使用終端界面輸入輸出的同時使用窗口上的菜單
來操作(但估計沒有人會做這樣的事情)。
控制臺程序最主要的用途是用于網絡的遠程維護。進行遠程維護時一般使用Telnet 等工
具登錄到遠程主機并在上面執行命令,如果執行的是圖形界面的程序,這個界面是無法遠程
操作的,所以我們可以發現Windows 中用于網絡的命令大多數是控制臺界面的,如Ping,
Netstat,Tracert,Arp,Route,Ipconfig 和Finger 等,與此相比,很難想像類似于Office 這樣
的軟件會用在遠程操作中。
作為對第4 章中窗口模式的補充,本節中將簡單介紹控制臺程序和窗口程序的區別,以
及控制臺程序的寫法。
A.1 控制臺程序和窗口程序的區別
除了和界面相關的代碼有所不同外,控制臺程序和窗口程序的區別還在于鏈接的時候指
定參數的不同,讀者一定還記得LINK 程序有個subsystem 參數,當這個參數指定為Windows
的時候,鏈接器生成的是窗口程序,本書中絕大部分以窗口為界面的例子程序中,LINK 語
句是這樣寫的:
Link /subsystem:windows Test.obj Test.res
將subsystem 參數改為console 的時候,LINK 程序產生的就是控制臺文件:
Link /subsystem:console Test.obj Test.res
兩種參數生成的可執行文件的不同表現在文件頭中,可執行文件(PE 文件)的文件頭中
有一個IMAGE_OPTIONAL_HEADER32 結構,結構中的Subsystem 字段就記錄了文件類型
的不同,讀者可以在第17 章的17.1.3 節中看到對文件頭的詳細分析。
運行文件時,操作系統會檢查文件頭中的Subsystem 參數,如果發現參數的類型是窗口
文件,那么將文件以正常的方式運行;如果發現參數的類型是控制臺文件,那么操作系統將
為程序創建一個控制臺窗口(即類似于DOS 窗口的這個文本窗口),然后運行文件。
另外,當一個控制臺程序是被另一個控制臺程序作為子進程運行的時候,系統不會為它
創建新的控制臺窗口,而是將父進程的窗口指定給它,所以在“我的電腦”中雙擊運行一個
控制臺程序的時候,會出現一個新的控制臺窗口,而在“命令提示符”窗口中用命令行參數
運行一個控制臺程序的時候,程序會直接使用“命令提示符”的窗口。
我們可以用幾個簡單的實驗來驗證這一點。首先打開“命令提示符”,進入第4 章例子程
序的目錄Chapter04\FirstWindow (這是一個普通的窗口程序而不是控制臺程序),在命令行下
輸入FirstWindow 來運行程序,程序運行后窗口出現了,但是不必等到窗口關閉,“命令提示
符”就會直接回到等待輸入命令的狀態,也就是說,普通的窗口程序并不會占用父進程的控
制臺窗口。
現在修改Chapter04\FirstWindow 目錄中的Makefile 文件,將LINK 命令的參數改成
/subsystem:console ,然后用nmake /a 重新編譯,這樣程序的代碼沒有任何變化,僅僅是它的
文件類型變成了控制臺程序而已。
重復上面的步驟,在命令行下運行FirstWindow 程序,可以看到,程序運行后窗口出現
了,但是“命令提示符”處于等待狀態,只有關閉窗口FirstWindow 程序,“命令提示符”中
才會回到等待輸入的狀態,這說明控制臺程序的父進程如果也是控制臺程序的時候,程序將
繼承父進程的控制臺窗口。
現在在“程序管理器”中通過雙擊FirstWindow.exe 文件來運行,一個正常窗口出現的同
時也出現了一個新的文本窗口,但是源代碼中并沒有創建過這個窗口呀?原來這個窗口就是
操作系統自動“搭配”給程序的控制臺窗口,關閉窗口退出程序后,控制臺窗口也同時消失。
這說明了當父進程不是控制臺程序的時候,操作系統會自動為控制臺程序創建一個控制臺窗
口。
所以,除了操作系統會在上述方面對控制臺窗口的創建或繼承進行一些準備工作外,控
制臺程序和窗口程序在其他方面并沒有什么不同,控制臺程序中仍然可以有消息循環,可以
創建窗口,也可以做窗口程序能做的任何事情。
A.2 書寫控制臺程序
現在用一個例子來說明如何在控制臺程序中進行輸入及輸出,例子程序的源代碼位于
Appendix A\EchoLine 目錄中,程序運行后,會等待用戶輸入一些字符,當用戶按下回車鍵后,
程序讀入輸入的行并以藍色將輸入的內容重新顯示出來,然后等待用戶輸入新的一行并循環
往復。如果用戶按下Ctrl+C 組合鍵,程序將退出執行。
程序的界面如圖A.1 所示,讀者可以注意到,例子程序的控制臺窗口的標題是“EchoLine
例子”。如果在“命令行提示符”中運行這個程序,窗口的標題也會從“命令提示符”變成
“EchoLine 例子”,等程序退出以后,標題會恢復到“命令提示符”。
圖A.1 EchoLine 例子程序的界面
例子程序的源代碼如下:
.386
.model flat, stdcall
option casemap :none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Include
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
include kernel32.inc
includelib kernel32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 數據段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data?
hStdIn dd ? ;控制臺輸入句柄
hStdOut dd ? ;控制臺輸出句柄
szBuffer db 1024 dup (?)
dwBytesRead dd ?
dwBytesWrite dd ?
.const
szTitle db 'EchoLine 例子',0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 代碼段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 控制臺 Ctrl+C 捕獲例程
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_CtrlHandler proc _dwCtrlType
pushad
mov eax,_dwCtrlType
.if eax == CTRL_C_EVENT || eax == CTRL_BREAK_EVENT
invoke CloseHandle,hStdIn
.endif
popad
mov eax,TRUE
ret
_CtrlHandler endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
start:
;********************************************************************
; 獲取控制臺句柄、設置句柄屬性
;********************************************************************
invoke GetStdHandle,STD_INPUT_HANDLE
mov hStdIn,eax
invoke GetStdHandle,STD_OUTPUT_HANDLE
mov hStdOut,eax
invoke SetConsoleMode,hStdIn,ENABLE_LINE_INPUT or \
ENABLE_ECHO_INPUT or ENABLE_PROCESSED_INPUT
invoke SetConsoleCtrlHandler,addr _CtrlHandler,TRUE
invoke SetConsoleTitle,addr szTitle
;********************************************************************
; 循環讀取控制臺輸入并顯示
;********************************************************************
.while TRUE
invoke SetConsoleTextAttribute,hStdOut,FOREGROUND_RED\
or FOREGROUND_GREEN or FOREGROUND_BLUE
invoke ReadConsole,hStdIn,addr szBuffer,\
sizeof szBuffer,addr dwBytesRead,NULL
.break .if ! eax
invoke SetConsoleTextAttribute,hStdOut,\
FOREGROUND_BLUE or FOREGROUND_INTENSITY
invoke WriteConsole,hStdOut,addr szBuffer,\
dwBytesRead,addr dwBytesWrite,NULL
.endw
invoke ExitProcess,NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start
控制臺程序輸入輸出的表現和DOS 程序類似,它可以從標準輸入設備進行輸入,也可
以輸出到標準輸出設備或者標準錯誤輸出設備,所以,控制臺程序中首先要做的事情是獲取
這些設備的句柄。
1. 控制臺句柄的獲取和設置
控制臺的各種句柄可以用GetStdHandle 函數來獲取,函數的用法如下:
invoke GetStdHandle,nStdHandle
.if eax != INVALID_HANDLE_VALUE
mov hStd,eax
.endif
將nStdHandle 參數指定為以下不同的取值即可獲取不同類型的句柄。
● STD_INPUT_HANDLE ——標準輸入句柄。
● STD_OUTPUT_HANDLE ——標準輸出句柄。
● STD_ERROR_HANDLE ——標準出錯信息句柄。
函數執行成功將返回對應的句柄,否則將返回INVALID_HANDLE_VALUE 值。在例子
程序中,程序兩次調用GetStdHandle 函數來分別獲取輸入和輸出句柄,并保存到hStdIn 和
hStdOut 變量中以便在以后用來讀取鍵盤輸入,以及進行屏幕輸出,對應的代碼如下:
invoke GetStdHandle,STD_INPUT_HANDLE
mov hStdIn,eax
invoke GetStdHandle,STD_OUTPUT_HANDLE
mov hStdOut,eax
...
獲取句柄后就可以用ReadConsole 函數來讀取鍵盤輸入了,但在此之前,可以用
SetConsoleMode 函數對輸入句柄的工作模式進行設置,這樣能讓ReadConsole 函數以我們所
期望的方式工作,如一次讀取一行還是讀取一個字符,是否對Ctrl+C 組合鍵進行攔截等。
SetConsoleMode 函數的用法如下:
invoke SetConsoleMode,hConsoleHandle,dwMode
hConsoleHandle 參數是要設置的控制臺句柄,dwMode 是工作模式,當控制臺句柄是輸
入句柄時,dwMode 可以指定為以下標志位的組合。
● ENABLE_LINE_INPUT ——行模式標志,指定該標志位后ReadConsole 函數將在用戶
輸入回車后才返回,否則用戶輸入任何字符后即返回。
● ENABLE_ECHO_INPUT ——指定該標志后,用戶輸入的時候字符將在屏幕上回顯。
● ENABLE_PROCESSED_INPUT ——指定該標志后,系統將攔截Ctrl+C 組合鍵,如果
不指定該標志的話,系統將Ctrl+C 組合鍵的鍵值(03h)作為字符返回給ReadConsole
函數。
● ENABLE_WINDOW_INPUT 和ENABLE_MOUSE_INPUT——當用戶對控制臺窗口
的大小進行改變,或者按動鼠標時,系統將記錄這些消息并允許程序用
ReadConsoleInput 函數讀取(ReadConsole 函數會忽略這種類型的消息)。
例子程序中對輸入句柄的模式進行設置時指定了三個標志位:ENABLE_LINE_INPUT,
ENABLE_ECHO_INPUT 和ENABLE_PROCESSED_INPUT ,表示程序在后面將以行模式讀
取鍵盤輸入,用戶輸入的同時需要將字符回顯并且程序將過濾Ctrl+C 組合鍵,對應的代碼
如下:
invoke SetConsoleMode,hStdIn,ENABLE_LINE_INPUT or \
ENABLE_ECHO_INPUT or ENABLE_PROCESSED_INPUT
例子程序中還用到SetConsoleTitle 函數將控制臺窗口的標題設置為“EchoLine 例子”,
SetConsoleTitle 函數只有一個參數——指向標題字符串的指針:
invoke SetConsoleTitle,addr szTitle
2. 截獲Ctrl+Break
在控制臺程序中往往需要截獲Ctrl+C(或Ctrl+Break)的組合鍵來判斷是否要中途退
出,這在DOS 時代的程序中靠截獲Int 23h 中斷來實現,但在Win32 中不能再使用這種方法。
Win32 控制臺程序使用SetConsoleCtrlHandler 函數來將Ctrl+C 的處理代碼重新定義到自
己指定的子程序中,這樣當輸入句柄具有ENABLE_PROCESSED_INPUT 屬性時,用戶按下
Ctrl+C 組合鍵后系統即調用指定的子程序:
invoke SetConsoleCtrlHandler,HandlerRoutine,Add
當HandlerRoutine 參數指定為處理Ctrl+C 按鍵的子程序地址,Add 參數指定為TRUE 的
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -