汇编网首页登录博客注册
mywiil的学习博客
博客首页博客互动【做检测题】论坛求助

我的博客

个人首页 |  我的文章 |  我的相册 |  我的好友 |  最新访客 |  文章收藏 |  论坛提问 |  友情链接 |  给我留言  
图片载入中
学习动态
好友圈
友情链接

[2009-03-25 10:19] 推荐博文 综合研究试验5--函数如何接收不定数量的参数

  这个研究试验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环境下进行编译、连接通过,运行显示正确。

======================================================
到现在为止,综合研究算是草草的学习了一边,跟着书中的思维方式走了一遭,不过,还有些乱,准备回头在将知识串一下,然后综合的思考一下这章内容是如何一步步组织深入的,也许就能明白作者的良苦用心了。

在整个学习过程中,肯定也有不少理解偏颇甚至错误的地方,希望兄弟们多多指教。
评论次数(3)  |  浏览次数(956)  |  类型(汇编作业) |  收藏此文  | 

[  游客   发表于  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页

刚刚来到这里,发现这个网站的学习风气特别好。人人都很虚心,认真。不像某些贴吧,论坛,整天就是攻击,谩骂,。

 
 请输入验证码  (提示:点击验证码输入框,以获取验证码