深入研究C语言 第二篇
1. 程序一:
首先我们研究如下程序:
回答如下问题:
1. 程序运行时n,a,b,c的段地址在哪个寄存器中?
全局变量的存储空间在什么段里?局部变量的存储空间在什么段了?参数在什么段里?函数的返回值存储在什么地方?
全局变量的存储空间在什么时候分配?什么时候释放?
局部变量的存储空间在什么时候分配?什么时候释放?
2. 函数f3在调用与返回方式与函数f1与f2有何不同?
我们编译完成后,进入debug查看。
首先,我们执行到main函数处,然后开始单步执行。我们看到,每次单步执行时,涉及到取数据或者数据赋值的操作,debug都会显示出要操作的地址的地址和当前值。我们先来验证这个地址和值是否是正确的:
由于源程序中赋值n为0,不好验证是否正确,我们将n的赋值暂时改为n=10;我们验证如下:
(语句执行后DS:01a6处的值)
(语句未执行前DS:01a6处的值)
可以看到,n=10这条语句执行时,debug右侧显示出要操作的地址为DS:01A6,而程序执行后,DS:01a6处的值变为了0A,也就是说,被赋值的n的地址就是这里。由此,我们可以确定,debug单步执行时的地址和当前值是可信的。
我们依据每次单步执行时的debug值来确定每个变量的位置:
这里,n=0;全局变量的地址为:DS:01A6,DS段值为0B96。
执行到此,我们看到的依然是全局变量n的地址。
继续执行,我们进行到f2函数调用时。我们看到, f2(1,2);中的参数,2和1分别被压入了栈中。并且其参数压入的顺序是从右向左压入的。其段地址当然是SS。
在f2中参数被调用时,是用BP加偏移量的方式被调用。最后时,将结果给了AX。在这里变量C就没有再内存中赋值,而是直接用SI、AX来保存的过程值。而参数被调用的时候,用的段寄存器是SS。
另外在退出f2函数的过程中,我们看到:
在入栈是分别是AX(第二个参数)、AX(第一个参数)、BP、SI。而出栈是只将SI、BP出栈,并没有将AX出栈。在这个时候,参数就被释放掉了。
然后程序就返回了,所以我们在此处确定,函数返回值return是通过AX返回的。
将n赋值时,将变量AX的值赋值给了n。
这里也是n的赋值。
我们在编写这样一个程序:
从这里我们更清晰的看出局部变量是定义在栈段里。而且,我们看到在函数一被调用时就有sub sp+02,所以我们可以看出,在程序调用的时候就分配下了变量的空间。
所以:程序运行时,n的段地址是DS,A的段地址是SS,B的段地址是SS,C没有在数据段中,使用的是寄存器AX。
全局变量的存储空间在数据段中,局部变量的存储空间在栈段里,参数的存储空间在栈段里,函数的返回值储存在AX中。
全局变量的存储空间在执行到声明变量的语句时分配,程序结束后被释放。
局部变量在函数被调用时分配,在函数结束后被释放。
参数的存储空间在调用函数时分配,在函数调用结束后释放。
我们对比f1和f3调用的不同,发现:1.调用时,f1是call 偏移地址,而f3是call 段地址+偏移地址。2.返回时,f1是直接RET,f3是先RETF再RET。
2. 程序二:
我们编写如下程序:
问题:变量n与a的存储空间分配方式有何不同?
编译完成后进入debug跟踪。
我们看到,A在程序开始就被分配,A在偏移位置为0194的位置存放。而N则是PUSH进栈,这时候才在栈中分配的,在这之前是在SI中存放的。而且,C程序的编译器对程序作了优化,将语句简化。
我们做验证:
编写程序:
首先我们看到,C语言在实现的时候,并没有按照我们在C语言中的语句顺序这样一条一条的翻译,而在底层的实现方式是,将n=2与n++放在一起。且变量n放置在寄存器SI中。
3. 程序三:
我们编写程序如下:
问题:
1. 程序中所有变量的存储空间相邻么?tc2.0中,整型、字符型、长整数型数据的存储空间分别为多大?
2. 不同的数据类型对数据运算方式有何影响?
Debug查看main函数执行时a、b、c、a1、a2的地址。
观察知:a、b、c、a1、a2的地址分别为:0194、0196、0198、0199、019B。
我们看到他们之间的差值分别是:2、2、1、2。我们知道int型变量的大小是2字节,所以可以看到,这些变量是连续的。
而且我们还看到,在int和char行数据++的时候,都是用的inc指令,而long型的是用的ADD指令。说明不同类型的数据所对应的数据运算方法不同。
我们看到这里,long型的变量,先用add,在用adc指令。这是因为long型变量长度是4字节,而汇编没有四字节的加法。只能先将低两位进行加法,然后再将有高两位与溢出标志位进行加法(这里是因为低两位相加的时候有可能溢出,如果溢出,应该向高两位进1,,而adc指令是带溢出位进行运算的。这样以来就可以保证高两位的值是正确的)。这样来完整的完成一次四字节的加法。
我们查询TC的变量长度:
我们可以推测,a2后的变量的地址为019F。我们验证:
这里也说明不同类型的变量是连续的,也可以说明long占用了4个字节。
4. 程序四:
编写程序如下:
问题:变量a,b和他们的各个数据项的存储空间是如何分配的?
首先我们分析自定义数据类型:它是由一个int型,三个char型变量共同组成的。这样一个数据类型应该再内存中占用5个字节。而两个自定义数据类型的变量a、b,他们一个是全局变量,一个是局部变量。在分配时,应当一个在数据段中,一个在栈段中。
编译完成后debug查看结构体内的变变量来进行验证:
我们看到,在自定义的结构体stu类型的全局变量a中,其各个数据项的排列是连续的。且整个变量a都在数据段,所占用的长度为5个字节。
在在自定义的结构体stu类型的局部变量b中,其各个数据项的排列依然是连续的。且整个变量都在栈段里,所占用的字节为5个。并且,在这里我们发现,虽然是对栈段做操作,但是这里没有用PUSH指令,用的也是MOV指令。并且我们看到,在程序一开始的时候,
Sp就做了相应的修改,把b的空间分配出来了。
5. 程序五:
我们编写如下函数,看结构体变量是如何传递和返回的:
反汇编如下:
我们看到,在汇编中有LEA这条指令,LEA就是目标地址传送指令: 将一个近地址指针写入到指定的寄存器。格式:LEA reg16,mem16。
在程序中,我们看到这这样的调用,结合上下文我们可以确定是调用的func函数。我们看其反汇编的代码:
我们看到,程序将0b28给了ax,但是我们知道,ax存放子函数返回值的寄存器。那么ax中放的是自定义的数据类型的变量么?显而易见,ax是放不下5个字节的。那么,又没有可能是偏移地址呢?我们查看。
果然,在数据段中我们找到了自定义类型a的存放地址。也就是说,func函数返回自定义类型的变量是靠返回其在数据段中的偏移地址返回的。
我们查看f函数的语句,看到f函数的偏移地址为0256。
我们找到调用语句:
我们看到,F中是用栈传入的结构体变量。
所以,我们可以知道,自定义数据类型的变量在函数中返回时,是靠返回其在数据段中的偏移地址返回的。而其当做参数被调用时是借助栈机制传入函数的。