罗云彬老师的<Windows环境下32位汇编语言程序设计>

书中十三章再讲Console下exe程序输入参数的问题引入了两个过程_argc和_argv,书中并没有给出详细解释,在这里总结了下。

这两个过程产生的原因是应为汇编不像C语言调用SDK在EP函数就自动帮助传入参数,大家都知道:

int APIENTRY main(HINSTANCE hInstance, 
                  HINSTANCE hPrevInstance, 
                  LPSTR lpCmdLine, //这个就是程序的命令行参数
                  int nCmdShow)

又或者在C语言CUI编程中:

int main(int argc, char **argv) {

    return 0;
}

其中argc就是参数的总数,而argv指向的就是包含每一个参数的指针数组

比如: C:\> nc 192.168.0.155 5750

那么argc就是3,argv分别是nc, 192.168.0.155, 5750

但是在汇编中这个过程不会帮我们做好。需要自己来做。可以自己写一个简单的程序调用GetCommandLine来获取输入的参数:

        	.386
		.model flat, stdcall
		option casemap :none

include		windows.inc
include		user32.inc
includelib	user32.lib
include		kernel32.inc
includelib	kernel32.lib

		.data?
szBuffer	db	1024 dup(?)
		.data
szTitle		db	\'GetCommandLine Results\', 0
		.code

start:
	invoke	RtlZeroMemory, addr szBuffer, sizeof szBuffer
	invoke	GetCommandLine
	mov	esi, eax
	invoke	MessageBox, NULL, esi, addr szTitle, MB_OK

	invoke	ExitProcess, NULL

	end	start

比如我在控制台输入:

由此可见GetCommandLine函数只帮我们做了传入全部参数的功能而非像argc计入参数总数,argv计入每个参数是什么

而_argc和_argv两个过程就是帮我们做这两个事情的

贴代码:

CHAR_BLANK	EQU	20h ;这是空格的Ascii的十六进制形式
CHAR_DELI	EQU	\'"\'
;----------------------------------------
_argc		proc
		local	@dwArgc
; * @dwArgc这个临时变量用于记录输入参数总数
;----------------------------------------
		pushad  ;通用寄存器入栈
		mov	@dwArgc, 0 ;首先置0
		invoke	GetCommandLine ;获取命令行,地址在eax中
		mov	esi, eax 
		cld ;清除方向寄存器
	_argc_loop:
		lodsb ;把命令行字符串的第一个字符载入al中
		or	al, al ;判断字符串以及到了最后一个字符即0
		jz	_argc_end ;如果ZF置位即已经到达最后一个字符
		cmp	al, CHAR_BLANK ;由于刚开始的时候可能有空格先判断空格
		jz	_argc_loop ;如果是空格那么继续往下载入字符直到碰到不是空格为止

		;-----------------------------------------------
		; 到这里代表已经碰到了第一个非空格字符
		; 由于lodsb类似C语言中的i++,先传值后挪地址
		; 所以esi要减1回到前面判断为非空格的字符那里
		;-----------------------------------------------
		dec	esi 
		inc	@dwArgc ;是第一个参数了就是那个a.exe
		
	_argc_loop1: ;这个循环是处理参数的,直到碰到空格才停
		lodsb
		or	al, al ;首先判断该参数是不是最后一个参数即跟了0字符
		jz	_argc_end
		cmp	al, CHAR_BLANK
		jz	_argc_loop ;碰到空格就停
		cmp	al, CHAR_DELI
		jnz	_argc_loop1 ;如果碰到了"号
		;---------------------------------------------------
		; 大家都知道cmd下是允许用引号的,比如:cd "File Programs"
		; 如果你不加引号那么OS会以为File和Programs是两个参数,或者
		; 你也可以这样cd File" "Programs,只要不要有空格就可以了
		; 所以如果你碰到了"号必然是成双的
		; --------------------------------------------------
	@@:	;这部分是为了判断引号中的内容,即引号中有空格不会增加参数数目
		lodsb
		or	al, al ;;首先判断该参数是不是最后一个参数即跟了0字符
		jz	_argc_end
		cmp	al, CHAR_DELI
		jnz	@B ;没碰到了第二个引号所以继续判断引号中内容
		jmp	_argc_loop1 ;引号结束跳回
	_argc_end:
		popad
		mov	eax, @dwArgc
		ret

_argc		endp
;----------------------------------------------
_argv		proc,
		_dwArgv:dword,
		_lpReturn:ptr byte,
		_dwSize:dword
		local	@dwArgv, @dwFlag
; _dwArgv: 所要得到的参数的下标
; _lpReturn: 参数内容的返回地址
; _dwSize: _lpReturn的大小
; @dwArgv: 主要为了确定目前参数是不是_dwArgv所指定的那个
; @dwFlag: 如果是_dwArgv所指定的那个就置位
;-----------------------------------------------
		
		pushad ;保护通用寄存器不用说了
		inc	_dwArgv ;因为一般下标都是0开始的,这里为了@dwArgv判断方便变为1
		mov	@dwArgv, 0 ;计数器清除
		mov	edi, _lpReturn 

		invoke	GetCommandLine ;下面和_argv中的如出一辙的代码不解释
		mov	esi, eax
		cld 

	_argv_loop: 
		lodsb ;这一段主要是为了清除一开始那些没用的空格键
		or	al, al
		jz	_argv_end
		cmp	al, CHAR_BLANK
		jz	_argv_loop
		
		dec	esi ;空格清除完毕,碰到第一个字符
		inc	@dwArgv ;计数器增加,意味着已经是第@dwArgv个参数了
		mov	@dwFlag, FALSE ;先把标志清空,这个标志用于判断当前参数是不是dwArgv所指定的那个
		mov	eax, _dwArgv 
		cmp	eax, @dwArgv ;看当前参数是不是所要求的那个
		jnz	@F
		mov	@dwFlag, TRUE ;如果是的话那就让标志置位
	@@:
	_argv_loop1:
		lodsb
		or	al, al
		jz	_argv_end
		cmp	al, CHAR_BLANK
		jz	_argv_loop ;碰到空格,说明这个参数结束,跳回
		cmp	al, CHAR_DELI ;看是否是"号
		jz	_argv_loop2 ;跳转至下一个标号来处理"号
		cmp	_dwSize, 1
		;------------------------------------------------------------------
		; 这边需要解释一下,这个_dwSize的含义是_lpReturn缓冲区的大小,如果
		; 当前参数是所要求的那个,那么就开始把这个参数一个字节一个字节的
		; 往缓冲区中填充,并且同时减少_dwSize,这里用cmp _dwSize, 1而不是
		; cmp _dwSize, 0是因为字符串最后一个要留着填充空字符来结束字符串
		;-------------------------------------------------------------------
		jle	@F
		;如果已经到了缓冲区能容纳的最大字符数但是还没有结束,那么后面的那些全部作废
		;不继续stosb装如缓冲区
		cmp	@dwFlag, TRUE
		jne	@F
		;如果不是所要求的参数那也不继续装入缓冲区
		stosb ;装货
		dec	_dwSize
	@@:
		jmp	_argv_loop1	

	_argv_loop2:
		lodsb
		or	al, al
		jz	_argv_end
		cmp	al, CHAR_DELI ;看有没有第二个"出现,如果出现了说明引号已经结束
		jz	_argv_loop1 ;跳回
		cmp	_dwSize, 1 
		jle	@F 
		cmp	@dwFlag, TRUE
		jne	@F 
		stosb
		dec	_dwSize
	@@:
		jmp	_argv_loop2

	_argv_end:
		xor	al, al ;把0字符装入最后一个位置来结束字符串
		stosb
		popad
		ret

_argv		endp

到这里这两个过程都已经解释完毕了

接下去就是两个宏了

先贴代码:

reverseArgs	MACRO	arglist:VARARG 
		LOCAL	txt,count
    
	txt	TEXTEQU	<>
	count	= 0
	FOR	i,<arglist>
	        count	= count + 1
	        txt	TEXTEQU @CatStr(i,<!,>,<%txt>)
	ENDM
	IF	count GT 0
	        txt	SUBSTR  txt,1,@SizeStr(%txt)-1
	ENDIF
	EXITM	txt
ENDM

_invoke		MACRO	_Proc,args:VARARG
		LOCAL	count
    
	count	= 0
%	FOR	i,< reverseArgs( args ) >
		count	= count + 1
		push	i
	ENDM
	call	dword ptr _Proc    
    
ENDM

这两个宏函数的意思非常简单我觉得困扰大家的并不是这个宏过程的含义而是里面的一些伪指令可能不知道什么意思。

我去查了Intel汇编程序设计里面结构与宏的部分, 里面也没有关于这些伪指令的解释

我用的是masm32 SDK包,在masm32\\help\\masm.chm这个文档中有详细解释

首先就是那个VARARG

看里面一堆好像很牛逼,实际上只是换了鸟文

VARARG属性允许你传递一个多个参数至宏中,当被调用的时候宏参数会传递一个逗号分隔的值列表(如果没有给予值那就是空格)

实际意思就是本来只能传一个参数,现在能传好多个。

接着是CATSTR:

这个CATSTR有两种用法,一种是宏函数,一种是直接用这个定义一个这样的变量,就像TEXTEQU和EQU伪指令做的一样

这里只解释代码中的宏函数用法

txt    TEXTEQU @CatStr(i,<!,>,<%txt>)

首先解释下!号类似于C语言中的转义符所以<!,>可以直接看成\',\'号就行了,为何加一个<>,因为!,是一个字符,但看上去是两个,实际上也是两个,为了让编译器看的懂,用<>括起来,代表我是一个

%txt的意思是取txt所代表的东西而不是txt本身

这里@CatStr(i, <!,>, <%txt>),意思实际上就是把i和,还有txt所代表东西黏起来。举个例子%txt = <1,>  i = 2 ,那么首先把2和,黏起来后变成2,然后黏到%txt前面就变成了<2,1,>。

接下去是SUBSTR:

这里没使用宏函数,使用了定义的方法

txt	SUBSTR  txt,1,@SizeStr(%txt)-1

最后那个长度参数是不必要的。如果没有指定长度参数 那返回的是所有字符

这个意思就是把txt定义成txt从第一个字符开始的@SizeStr(%txt) - 1个,也就是从屁股后面减掉一个。原因是上面CATSTR得到的结果是1,2, 但是2后面的那个逗号是不必要的

@SizeStr宏显而易见是类似于strlen()函数,做一下搬运工作:

%	for	i,< reverseArgs( args ) >

可能这句话也十分困扰大家吧,那个%号是用来干什么的?

%原本是展开操作符,为的是把常量表达式转成文本表示形式或者展开文本宏。

显而易见这里是展开文本宏,如果%作为源代码行的第一个字符, 就指示预处理器展开在该行所发现的所有文本宏和宏函数。

那个宏就是reverseArgs(args)

-----------------------------------END--------------------------------------

总结一下:

这两个宏的目的是首先reverseArgs是为了把通过GetCommandLine收到的参数字符串逆置,比如参数是1,2,3那就变成3,2,1

然后在通过_invoke来调用。至于为什么要逆置原因是每个函数都维护着一个帧栈,而参数都是反向压栈,这样的话在最后call的时候可以按顺序出栈,举一个例子:
 

#include <stdio.h>

int add(int a, int b, int c) {
    return a + b + c;
}

void main() {
    int a = 5, b = 2, c = 1;
    add(a, b, c);

    return;
}

实际上是(这里手写,可能和OD反编译出来的不用,但就是这个意思):

push   ebp
mov    ebp, esp
sub    esp, 0Ch
mov    dword ptr [ebp], 5
mov    dword ptr [ebp - 4], 2
mov    dword ptr [ebp - 8], 1
push   dword ptr [ebp - 8]
push   dword ptr [ebp - 4]
push   dword ptr [ebp]
call   0040FC30 ;假设add在0040FC30
mov    esp, ebp
pop    ebp
retn


0040FC30:
push   ebp
mov    ebp, esp
xor    eax, eax
mov    eax, [ebp + 8]
add    eax, [ebp + 12]
add    eax, [ebp + 16]
mov    esp, ebp
pop    ebp
retn

 

相关文章: