数组指针与二维数组的寻址(2)

其中第4行编译器为局部变量(auto)在栈上分配内存空间0x50字节,6~17行,编译器为二维数组x初始化,其中,x[0][0]的地址为%esp+10。19~22行分别为p,k,m,n初始化。(从中可以看出,p初始化使用了leal指令取第一个元素的地址,且p只占用了4个字节,也就是说,从数据大小来看,数组指针本质上还是一个指针)

现在想要研究编译器如何对数组指针进行操作,通过jle指令可以定位到循环为24~35行。在原始的C语言代码中,for循环的body-statement只有一句复合语句,最后的操作显然对应累加,也就是32行的add指令(33行的addl显然是计数器累加,因为34行用到了cmpl指令判断大小)。32行的add指令中,%esp+48对应变量n,31行用%eax的值作为地址进行寻址,将地址为%eax的值放进%eax中,显然对应C语言语句中最外层的一个*号。30行的add指令后的%eax的值显然便是表达式:*(p+m)+k的值。

重点在于理解编译器如何解析这个表达式了。24行取%esp+0x4c(m的值),25行用leal指令将m*4并放入%edx寄存器中,26行取%esp+0x40(k的值)放入寄存器%eax中,27行将%eax和%edx的值相加,得到整个的偏移地址4m+k,28行将整个偏移地址乘以4得到实际的字节偏移地址,29行再将其与数组第一个元素的地址相加,得到表达式*(p+m)+k的值了。因此,25行leal指令得到的系数4,恰好对应定义的数组指针的长度4。如果在原题中将(*p)[4]改为(*p)[3],于是编译器得到如下代码(仅截取循环内):

1 0x4013d0 mov 0x4c(%esp),%edx 2 0x4013d4 mov %edx,%eax 3 0x4013d6 add %eax,%eax 4 0x4013d8 add %eax,%edx 5 0x4013da mov 0x40(%esp),%eax 6 0x4013de add %edx,%eax 7 0x4013e0 lea 0x0(,%eax,4),%edx 8 0x4013e7 mov 0x44(%esp),%eax 9 0x4013eb add %edx,%eax 10 0x4013ed mov (%eax),%eax 11 0x4013ef add %eax,0x48(%esp) 12 0x4013f3 addl $0x1,0x4c(%esp) 13 0x4013f8 cmpl $0x1,0x4c(%esp) 14 0x4013fd jle 0x4013d0 <main+144>

这里编译器使用两条add指令计算数组长度3代替了原先的leal指令计算的数组长度4(编译器往往会选择合适的指令来减小开销,比如用移位和加法指令代替常数乘法,但是会使得汇编码和C代码的对应不是很明显),而后的代码与原先如出一辙。

可以看出,数组指针指向的是一个数组,数组指针进行自增,会将实际的地址指向下一个依靠的数组。由于二维数组在内存中实际也是按照“行优先”的规则映射到一维的线性的数组中来存储的,编译器在解释数组指针的过程中,会首先计算数组指针所指向的数组的长度(定义数组指针时确定),然后根据所指向的数组的长度计算偏移地址,将其与初始化的基地址(将其与一个二级指针关联时得到的基地址)相加,得到所指向的数组的第一个元素的地址。因此,数组指针的长度和与它相关联的实际的二维数组的行列长度并不需要严格一致,只是为了使用方便,往往会将数组指针所指向的数组的长度与实际需要操作的二维数组的行长度相对应。
事实上,访问二维数组D(定义为ElementType D[R][C])中的i行j列的元素时,通用的寻址方法是

&D[i][j]=xD+L(C·i+j),其中xD为二维数组的首地址,L为数组的元素数据类型的大小,C为定义的行长度。

数组指针的寻址本质上是一致的。在开头的例题里,公式中xD=p,i=m,j=k。

参考:深入理解计算机系统第二版,p158.3.8节 数组的分配与访问。

深入理解计算机系统(原书第2版) PDF清晰中文版  下载

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/f42a0f7cefb839b8ecae9791cebf70cc.html