1. 程式人生 > >王爽組合語言綜合研究-函式如何接收不定數量的引數

王爽組合語言綜合研究-函式如何接收不定數量的引數

1、c語言如何傳遞引數

編寫這樣一個程式試驗

void showchar(char a,int b);

main()
{
	showchar('a',2);
}

void showchar(char a,int b)
{
	*(char far *)(0xb8000000 + 160*10 + 80) = a;
	*(char far *)(0xb8000000 + 160*10 + 81) = b;
}
除錯這個程式

                        ;進入main程式
141A:01FA 55            PUSH    BP        ;儲存暫存器現場
141A:01FB 8BEC          MOV     BP,SP

141A:01FD B80200        MOV     AX,0002        ;將2個位元組的2h入棧
141A:0200 50            PUSH    AX
141A:0201 B061          MOV     AL,61        ;將1個位元組的'a'入棧
141A:0203 50            PUSH    AX
141A:0204 E80400        CALL    020B        ;呼叫子程式
141A:0207 59            POP     CX        ;釋放區域性變數的空間
141A:0208 59            POP     CX
141A:0209 5D            POP     BP        ;恢復暫存器現場
141A:020A C3            RET            ;main函式返回

                        ;進入子程式
141A:020B 55            PUSH    BP        ;儲存暫存器現場
141A:020C 8BEC          MOV     BP,SP       
141A:020E 8A4604        MOV     AL,[BP+04]    ;讀出字元'a'
141A:0211 BB00B8        MOV     BX,B800        ;寫入到b800:0690h
141A:0214 8EC3          MOV     ES,BX
141A:0216 BB9006        MOV     BX,0690
141A:0219 26            ES:
141A:021A 8807          MOV     [BX],AL
141A:021C 8A4606        MOV     AL,[BP+06]    ;讀出資料2h
141A:021F BB00B8        MOV     BX,B800        ;寫入到b800:0691h
141A:0222 8EC3          MOV     ES,BX
141A:0224 BB9106        MOV     BX,0691
141A:0227 26            ES:
141A:0228 8807          MOV     [BX],AL
141A:022A 5D            POP     BP        ;恢復暫存器現場
141A:022B C3            RET            ;子程式返回
141A:022C C3            RET

容易分析,c中呼叫函式是通過棧來傳遞引數的,呼叫前將引數從右往左依次入棧。
引數在函式中是區域性變數,這種方式和建立區域性變數的方式類似,可以認為是在子程式呼叫前為子程式建立區域性變數
所不同的是子程式裡區域性變數通過儲存和恢復sp暫存器來釋放區域性變數空間,引數的區域性變數必須通過呼叫完成後多次呼叫pop操作來釋放棧空間
2、c語言如何傳遞不定數量的引數
通過上文的分析,c程式在呼叫函式前,首先對所有引數依次入棧,在呼叫後依次出棧。
在函式內部,如果知道了有多少個引數,可以根據sp+偏移量來找到引數所在的位置,完成引數的接收
那麼如果是不定數量的引數呢,函式是怎麼知道有多少個引數的
研究下面的程式
void showchar(int,int,...);

main()
{
	showchar(8,2,'a','b','c','d','e','f','g','h');
}

void showchar(int n,int color,...)
{
	int a;
	for (a = 0; a!=n; a++)
	{
		*(char far *)(0xb8000000+160*10+80+a+a) = *(int *)(_BP+8+a+a);
		*(char far *)(0xb8000000+160*10+81+a+a) = color;
	}
}

在這個程式中,函式宣告成了void showchar(int,int,...),引數一傳入了一個字元的數量,引數二傳入顯示的顏色,引數三開始傳入要顯示的字元。函式通過引數一來控制顯示字元的迴圈次數,通過這種方式來接收多個引數

 3、printf函式接收引數的原理
編寫這樣一個簡單的程式
void main()
{
	printf("%c,%c,%c,%c,%c",'a','b','c','d','e');
}

除錯跟蹤呼叫printf的這段程式碼

1

看到編譯器將字元e(65h),d(64h),c(63h),b(62h),a(61h)入棧後,又將0194h入棧後呼叫的printf子程式

由上邊的分析,0194h肯定是和printf的第一個引數"%c,%c,%c,%c,%c"相關的一個數據,我們猜想可能是指向這樣一個字串的地址,下面驗證這個猜想

在debug下用g 215命令執行到偏移地址為215h的位置,也就是printf呼叫前。用d ds:0194命令檢視0194h處的記憶體,如圖

2

我們發現,編譯器確實把第一個引數放到了資料段中,並且把偏移地址入棧傳入printf函式中

那麼,printf函式是如何知道有多少個引數的呢?為了看的更清楚,修改上面的程式

void main()
{
	printf("%c,%c,%c,%c,%c",'a','b','c','d','e');
	printf("%d,%d",1,2);
}
這個程式進行了兩次printf呼叫,重複上面的分析過程,首先檢視兩次呼叫時入棧的地址
兩次呼叫時入棧的地址分別為0194h和01A3h,下面檢視這段記憶體空間
發現0194h指向的字串的十六進位制為:25 63 2c 25 63 2c 25 63 2c 25 63 2c 25 63 00
                 對應的字串是:%c,%c,%c,%c,%c
   01A3h指向的字串的十六進位制為:25 64 2c 25 64 00
                 對應的字串是:%d,%d
通過以上分析,發現字串後邊都有一個0作為結尾。
printf可能是根據傳入的%的個數來確定列印的字元數,讀入一個%就會讀取後面一個字元來確定列印的方式,當讀出一個0時列印結束
為了驗證這個猜想,我們在debug模式下修改這個字串,再看呼叫的結果
a、執行到printf函式呼叫之前,檢視0194h處的記憶體
b、我們把第二個%c後的‘,’對應的16進位制值2c修改為0(也就是我們剛才猜想的字串結束的標誌)。再次執行看是否只打印出兩個字元。
c、用debug的e命令修改0199h處的值為0,如圖
這個位元組的記憶體值已經修改成了0,用debug的g命令將程式執行完畢,螢幕上只打印出了兩個字元
這說明printf函式確實是通過字串的%個數來傳遞列印的字元數的

4、簡易的print函式

基於上述的基礎,可以自己寫一個print函式,用來在螢幕上列印字元

void print(int color,int row, int col, char * str, ...);

void main()
{
	print(2,13,30,"12345");
	print(3,14,30,"%c,%c,%c,%c",'a','b','c','d');
	print(4,15,30,"%d,%d",7,8);
	print(5,20,12,"%c,%d,123abc,%d",'z',13,3456);
}

/*引數列表:顏色,行,列,列印格式,附加引數1,附加引數2...*/
void print(int color,int row, int col, char * str, ...)
{
	int strnum = 0;	/*字串位置計數器*/
	int stacknum = 0;	/*棧字元位置計數器*/
	char ch = str[strnum++];			/*要處理的下一個字元*/
	int scrnum = 80*2*row + col*2;	/*視訊記憶體位置*/
	int quotient = 0;		/*儲存每次除法的商*/
	int pushnum = 0;		/*除法時入棧次數計數器*/
	while (ch)		/*如果下一個字元為0,則跳出迴圈*/
	{
		if (ch == '%')
		{
			/*如果ch是%,那麼先讀出下一個字元*/
			ch = str[strnum++];
			/*判斷下一個字元是c還是d,並分別處理*/
			switch (ch)
			{
				/*如果是c,按照字元型輸出棧中相應資料*/
				case 'c':
					*(char far *)(0xb8000000 + (scrnum++)) = *(int *)(_BP + 12 + (stacknum++));
					*(char far *)(0xb8000000 + (scrnum++)) = color;
					break;
					
				/*如果是d,按照十進位制整形輸出棧中相應的資料*/
				case 'd':
					pushnum = 0;
					quotient = *(int *)(_BP + 12 + (stacknum++));
					if (quotient == 0)
					{
						*(char far *)(0xb8000000 + (scrnum++)) = '0';
						*(char far *)(0xb8000000 + (scrnum++)) = color;
					}
					while(quotient)
					{
						_CX = quotient%10;
						_SP -= 2;			/*模擬入棧過程*/
						*(int *)(_SP) = _CX;
						pushnum++;
						quotient /= 10;
					}
					while(pushnum--)
					{
						_CX = *(int *)(_SP);       /*模擬出棧過程*/ 
						_SP += 2;
						*(char far *)(0xb8000000 + (scrnum++)) = _CL + 48;
						*(char far *)(0xb8000000 + (scrnum++)) = color;
					}
					break;
			}
			stacknum++;
		}
		else 		/*如果當前ch值不是%,那麼直接將ch寫入到視訊記憶體*/
		{
			*(char far *)(0xb8000000 + (scrnum++)) = ch;
			*(char far *)(0xb8000000 + (scrnum++)) = color;
		}
		
		/*讀取下一個字元*/
		ch = str[strnum++];
	}
}

編譯連線這個程式,執行

7

通過這個程式,已經可以理解了函式接受不定引數的原理