这个研究试验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环境下进行编译、连接通过,运行显示正确。
======================================================
到现在为止,综合研究算是草草的学习了一边,跟着书中的思维方式走了一遭,不过,还有些乱,准备回头在将知识串一下,然后综合的思考一下这章内容是如何一步步组织深入的,也许就能明白作者的良苦用心了。
在整个学习过程中,肯定也有不少理解偏颇甚至错误的地方,希望兄弟们多多指教。
- [fpamc] 欢迎回归,谢谢分享哈。。 08/21 07:20
- [parse] 声声入耳!佩服。 03/07 20:52
- [chinatree] 强文,收藏了。 02/23 11:17
- [mywiil] xml文件长一个模样,为的是统一数据标准。 02/03 09:36
- [chinatree] 感觉和html一样。 02/02 18:06
- [chinatree] 坐沙发看,虽然看不懂。 02/01 17:10
- [mywiil] 我也是记录下学习过程中的资料。 没事把几个框架都弄弄。用的着。 01/30 15:44
- [chinatree] 博主乃强人也!膜拜不已。 01/30 11:04
- [chinatree] 顶呀! 01/14 17:47
- [tomato] 顶! 01/14 11:20
- [zwjq] 谢谢检查我的作业,非常感谢! 08/21 14:10
- [zouhehui] 谢谢你帮我检查作业,非常感谢! 12/02 12:46
- [assem] 老兄的个性签名很有意思,O(∩_∩)O~,学习了...感谢你对我的提醒,我会加油的,谢过了! 09/19 12:24
- [miaozaoyang] 呵呵,还好,学校挺支持大家学习的。 不过还是有很多的问题啊。 主要还是自学。。。。。。 08/19 23:18
- [semidotnet] 谢谢你对我作业的检查^_^ \(^o^)/ 欢迎下次再来我博客 08/18 10:39
- [zdpopup] 谢谢,你一直的帮助 07/22 09:41
- [fang] 地址总线宽度别为16根,20根,24根,32根 寻址能力分别为:65KB 1MB 16MB 4GB 07/21 09:37
- [zdpopup] 谢谢你帮我改了不少题 和提了很多有用的提示. 谢谢 06/28 23:49
- [lihongbo6668201] 谢谢你啊,但是怎么可以在输入的时候就把62627转换成16进制的送入ax ?我自己转换了一下果然是F 06/28 11:32
- [游客] 15天精通C语言 04/25 11:43
[ 游客 发表于 2009-03-25 13:40 ]
等等,太长了,看的有点晕乎~~
[ mess 发表于 2009-04-08 13:30 ]
[ 游客 发表于 2009-03-25 13:40 ]
等等,太长了,看的有点晕乎~~
-----------------
如果晕乎,那说明没有一步步的看综合研究,这个实验也没有自己试验就看了,光看不动手当然得糊涂了啊~
[ 游客 发表于 2009-05-25 10:37 ]
在这里,我们发现一点,main函数传入showchar的那些字符的位置是在ds段的,也就是说*(int *)(_BP+08+a+a)这里取值是从ds:[bp+08+a+a]这里取得的
-----
如果偏移地址在bp中,默认的段地址在ss中。(参见王爽汇编)149页
刚刚来到这里,发现这个网站的学习风气特别好。人人都很虚心,认真。不像某些贴吧,论坛,整天就是攻击,谩骂,。