|
主题 : : 【砖头】综合研究试验5--函数如何接收不定数量的参数 [待解决] |
回复[ 7次 ]
点击[ 1636次 ] | |
|
|
|
|
[帖 主]
[ 发表时间:2009-03-27 15:31 ]
[引用]
[回复]
[ top ] | |
荣誉值:61
信誉值:4
注册日期:2008-10-14 16:29 |
这个研究试验5是最短的一个研究试验了,但是内容可不简单,也挺费脑筋的(也许是我脑子笨了点)。
刚一看,基本没思路,什么叫不定数量的参数,以前好像没注意过,写函数都是既定的参数啊。结果一看内容,突然知道了什么叫做熟视无睹了。C语言中经常使用的printf这个函数原来就是不定参数的。看来自己的洞察力不够,以前总是打印,打印,打印啊的,没发现printf函数不受参数个数的限制。晕菜~~怎么就能解决这未知的东西呢?程序可没有思想啊,也不能随机应变啊。想不明白啊!跟着书上的路学吧。
首先,是对一个显示字符的简单程序的分析。这个程序经过debug跟踪,main函数和showchar函数汇编代码如下:
---------------------------------
以下是main函数对应的汇编代码
---------------------------------
194C:01FA 55 PUSH BP
;将bp压栈,保存该值
194C:01FB 8BEC MOV BP,SP
194C:01FD B80200 MOV AX,0002
194C:0200 50 PUSH AX
;将调用showchar时传入的参数2压栈。可以看出,传入函数的参数,是从右到左开始压栈的。
194C:0201 B061 MOV AL,61
194C:0203 50 PUSH AX
;将调用showchar时传入的参数a压栈。a的ascii码为61H
194C:0204 E80400 CALL 020B
;调用showchar函数,我们根据ip=020b去看一下showchar函数。在这里别忘了有一个push ip的压栈操作。
194C:0207 59 POP CX
194C:0208 59 POP CX
;调用完showchar函数,sp又回到了push ax后的值,也就是指向了栈中压入的最后一个参数的位置,因为有两个参数,而且参数已经完成使用了,所以,这些参数没必要占用栈空间了,就pop掉。
194C:0209 5D POP BP
;恢复bp的原始值
194C:020A C3 RET
---------------------------------
以下是showchar函数对应的汇编代码
---------------------------------
194C:020B 55 PUSH BP
;进入函数的时候,将bp压栈,因为后面要使用bp定为栈的内存单元。
194C:020C 8BEC MOV BP,SP
194C:020E 8A4604 MOV AL,[BP+04]
;将ss:[bp+4]对应的一个字节送入al,那[bp+4]的值是什么呢?我们从此处往后倒,看看这个位置是什么。首先由于mov bp,sp,所以bp此时指向了栈底,那么ss:[bp] = bp(showchar函数的push bp压入的),ss:[bp+2]=IP(main函数中call 020b指令压入的IP),ss:[sp+4] = 61H='a' (main函数中push ax压入的‘a’)。这下就知道了,ss:[bp+4]对应的数据是main函数传的参数a,由于a占一个字节,而栈操作是字操作,所以,高位就用0填了,不过,由于这里mov al,[bp+4]是字节操作,所以,al取得该栈字单元中的低8位,也就是al=61H。说道这里,就有点醒悟了,参数的传递,是通过栈来进行的。那么,按这个道理,下一个参数应该是ss:[bp+6]的地址,到底是不是,我们往下看。
194C:0211 BB00B8 MOV BX,B800
194C:0214 8EC3 MOV ES,BX
194C:0216 BB9006 MOV BX,0690
194C:0219 26 ES:
194C:021A 8807 MOV [BX],AL
;将al中的值61H写入显存
194C:021C 8A4606 MOV AL,[BP+06]
;这里是取得main函数传入的第二个参数2的,我们根据前面的分析,发现此处的操作地址正是ss:[bp+6],也再次证明了,我们的推断是正确的:函数参数的传递是通过栈来实现的。
194C:021F BB00B8 MOV BX,B800
194C:0222 8EC3 MOV ES,BX
194C:0224 BB9106 MOV BX,0691
194C:0227 26 ES:
194C:0228 8807 MOV [BX],AL
;将al中的值2写入显存
194C:022A 5D POP BP
194C:022B C3 RET
【结论】
1.main函数是通过栈来向其调用的函数传参数的,main函数将参数反向依次压入栈中。
2.被调用函数showchar是从栈中接受的调用者传入的参数的,使用bp在栈中进行寻址定位,将main函数传入的参数正向依次取出进行使用。
接下来,是实践程序(2)的学习和理解,在前面程序理解的基础上,体会一下对于函数处理参数个数的这个问题点。
源程序以及相关函数对应的汇编代码就不贴上来了,在分析过程中只把关键的代码拿来进行分析和说明。
我们已经知道了,函数传递参数是靠栈来进行的。我们只看showchar函数怎么使用参数的。为了流程上符合程序运行的顺序,我们用T命令,单步执行从main函数开始的程序。
在进入showchar对应的代码中,首先遇到的指令为
194C:027E 3B7604 CMP SI,[BP+04]
这里si保存的是变量a的值,我们看源程序,可以知道,a是用来和n来比较的,所以,[bp+4]应该是对应的参数n的值,根据前面程序的经验,可以肯定,[bp+4]就是对应的showchar形式参数中的第一个参数 int n。程序中的for循环就是通过这个指令来进行循环,控制显示字符的个数的,也就是通过这个指令和操作数据来知道显示多少个字符的。
我们继续向下,根据源程序,应该是想显存中写入显示字符的步骤了,关键的赋值代码为
194C:023E 8A07 MOV AL,[BX] DS:FFD8=61
这里就是将参数的值放入al值,以待后面将al值放入显存对应的位置,进行显示。在这条指令之前,有一个结果相当于bp+08+a+a的地址计算。在这里,我们发现一点,main函数传入showchar的那些字符的位置是在ds段的,也就是说*(int *)(_BP+08+a+a)这里取值是从ds:[bp+08+a+a]这里取得的。但是,我们观察寄存器可以看到ds和ss的段地址是一样,所以,这里也就是从栈对应的内存中取得的参数。那这里的8是怎么计算出来的呢?我们知道bp+4是showchar的第一个参数,而前两个都是int型(tc2.0下int型占一个字),所以,第三个参数的位置就是bp+8,而第三个参数就是所要显示的字符的第一个,以后依次是bp+10,bp+12...由于有一个+a+a的操作,位的就是调整取得参数的位置,这个自己体会,思考一下就可以明白了。在这里我们知道了,通过计算参数所占内存的大小,直接定位到栈内我们需要操作的参数的位置。
通过前面的思维方式,接下来将显示颜色放入显存的操作作用,对于color变量的定位,应该是bp+4+2,跳过int n这个参数占用的2个字节的内存空间,我们可以看一下相应的汇编代码来验证,代码如下
194C:025D 8A4606 MOV AL,[BP+06] SS:FFD6=02
说明我们的理解是正确。
行了,到了这里,这个程序b.c中的关键部分已经分析完毕了。showchar函数是通过获取第一个参数int n的大小来控制循环来控制显示多少个字符。那么,我们在printf中似乎没有看到过类似先要指定参数个数的参数的,那么printf是怎么控制显示字符的个数呢?我们知道printf函数的第一个参数是一个字符串,这个字符串中有一个特殊的符号,如“%c”、“%d”等,这些“%*”就是用来控制显示制式的,printf就是个根据字符串中这种字符的个数来控制显示字符的个数的。比如说,printf("%c,%c",'a','b'),有两个“%*”,所以要显示两个字符。
我们要实现一个printf,首先,我们要能够找到这个函数中的每个参数。
定义该函数的形式如下 myprintf(char * , ...)
由于是不定参数,所以,得声明成上面的那种形式,就像程序b.c中showchar的声明方式,这就是C支持的声明不定参数的函数的格式。
我们主要解决定位参数的问题,首先char *这个参数我们应该清晰的知道了,那就是bp+4,而char *是一个指针类型,也就是一个偏移地址,所以,这个参数占用2个字节,也就是说,参数“...”第一个参数的位置应该就是bp+6。参数能够定位了,那么接下来就是代码的编写了。
以下是我实现的一个简单的myprintf。(因为在输出%d的时候,没有做对整数进行处理的操作,类似汇编中将12666显示出来的那个实验的分割数位操作,所以,这里的%d输出仅仅对0~9这10个数字能够正确处理,由于该题目的目的在于理解不定参数函数,所以,就不在实现数位分割的程序了,也算偷懒了吧。呵呵)
void myprintf(char * , ...);
main(){
myprintf("abc%c:123%c:8%d10",'d','4',20);
}
void myprintf(char * s, ...){
char * p;
int pos ;/*控制要显示的参数的寻址*/
int rpos;/*控制显存中每次显示字符的位置的定位*/
p = *(int *)(_BP+4);/*这里可以简化成p = s ,但为了理解参数定位,所以这里用这个形式*/
pos = 0;
rpos = 0;
while(*p != 0){
if(*p !='%'){
*(char far *)(0xb8000000+160*10+80+rpos) = *p;
}else{
if(*(p+1) == 'c'){
p++;
*(char far *)(0xb8000000+160*10+80+rpos) = *(char *)(_BP+6+pos);
pos+=2;
}else if(*(p+1) == 'd'){
p++;
/*这里只支持0~9数字的显示,因为直接给数值0~9加上了30H来转化成字符*/
*(char far *)(0xb8000000+160*10+80+rpos) = (char)(*(int *)(_BP+6+pos)+0x30);
pos+=2;
}else{
printf("%c",*p);
}
}
p++;
rpos += 2;
}
}
在minic环境下进行编译、连接通过,运行显示正确。
======================================================
到现在为止,综合研究算是草草的学习了一边,跟着书中的思维方式走了一遭,不过,还有些乱,准备回头在将知识串一下,然后综合的思考一下这章内容是如何一步步组织深入的,也许就能明白作者的良苦用心了。
在整个学习过程中,肯定也有不少理解偏颇甚至错误的地方,希望兄弟们多多指教。 | | |
|
|
|
|
[第1楼]
[ 回复时间:2009-04-03 20:40 ]
[引用]
[回复]
[ top ] | |
荣誉值:16
信誉值:0
注册日期:2008-10-12 13:46 |
|
|
|
|
|
[第2楼]
[ 回复时间:2009-04-10 17:36 ]
[引用]
[回复]
[ top ] | |
荣誉值:8
信誉值:0
注册日期:2008-12-18 16:28 |
|
|
|
|
|
[第3楼]
[ 回复时间:2009-04-13 16:33 ]
[引用]
[回复]
[ top ] | |
荣誉值:0
信誉值:0
注册日期:2007-10-17 09:53 |
一, /*这里只支持0~9数字的显示,因为直接给数值0~9加上了30H来转化成字符*/
*(char far *)(0xb8000000+160*10+80+rpos) = (char)(*(int *)(_BP+6+pos)+0x30);
楼主的这一句足见功夫,不如.
二,(_BP+4)这里面是第一个字符串的首地址
..........
谢谢!! | | |
|
|
|
|
[第4楼]
[ 回复时间:2009-05-05 20:34 ]
[引用]
[回复]
[ top ] | |
荣誉值:0
信誉值:0
注册日期:2009-05-03 13:21 |
push bp 结果就是sp-2
mov bp,sp 结果就是sp-2等于 bp=sp-2
mov ax,0002
push ax 结果就是sp-4 bp=sp-2
mov al,61
push ax 结果就是sp-6 bp=sp-2
CALL 020B 结果就是sp-8 bp=sp-2
---------------------------------
以下是showchar函数对应的汇编代码
---------------------------------
PUSH BP 结果就是sp-10 bp=sp-2(上一个main函数中的bp=sp-2压栈保存)
MOV BP,SP 结果就是 bp=sp-10
MOV AL,[BP+04] 结果就是mov al,[sp-10+04]=mov al,[sp-6]
看上一个main函数中(push ax 结果就是sp-6)
所以:
mov al,[bp+04]就是对应的一个字节送入al | | |
|
|
|
|
[第5楼]
[ 回复时间:2010-05-14 16:57 ]
[引用]
[回复]
[ top ] | |
荣誉值:0
信誉值:6
注册日期:2009-07-15 11:17 |
引用:
194C:0207 59 POP CX
194C:0208 59 POP CX
;调用完showchar函数,sp又回到了push ax后的值,也就是指向了栈中压入的最后一个参数的位置,因为有两个参数,而且参数已经完成使用了,所以,这些参数没必要占用栈空间了,就pop掉。
-----------------------------------------------------------------
以前“pop cx”不知道是干嘛用的,今天算是学习了。 | | |
|
|
|
|
[第6楼]
[ 回复时间:2010-05-23 12:21 ]
[引用]
[回复]
[ top ] | |
荣誉值:0
信誉值:0
注册日期:2010-04-01 20:41 |
|
|
|
|
|
[第7楼]
[ 回复时间:2014-02-25 12:12 ]
[引用]
[回复]
[ top ] | |
荣誉值:0
信誉值:0
注册日期:2013-02-02 10:57 |
修改了以下楼主的代码,看看怎样?
void myprintf(char *,...);
main()
{
myprintf("[%c][%c][%d][%a]",'a','b',1);
}
void myprintf(char *s,...)
{
char *p;
int pos = 0;
int vpos = 0;
p = s;
pos = _BP+6;
while(*p != 0)
{
if(*p != '%')
{
*(char far *)(0xb8000000+160*2+vpos) = *p;
}
else
{
switch (*(p+1))
{
case 'c':
p++;
*(char far *)(0xb8000000+160*2+vpos) = *(char *)pos;
pos += 2;
break;
case 'd':
p++;
*(char far *)(0xb8000000+160*2+vpos) = (char)(*(int *)pos+0x30);
pos += 2;
break;
default:
*(char far *)(0xb8000000+160*2+vpos) = *p;
break;
}
}
p++;
vpos += 2;
}
} | | |