王爽組合語言綜合研究-函式如何接收不定數量的引數
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,SP141A: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的這段程式碼
看到編譯器將字元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處的記憶體,如圖
我們發現,編譯器確實把第一個引數放到了資料段中,並且把偏移地址入棧傳入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++]; } }
編譯連線這個程式,執行
通過這個程式,已經可以理解了函式接受不定引數的原理