这一节我讨论下数组和指针的汇编实现,以及在C程序中嵌入简单汇编代码的内容,至于条件判断、循环、goto等内容,资料很多且比较程式化,就不再涉及了。
一:数组的汇编实现
对指针和数组理解的深入程度是衡量人C语言水平的分水岭,其概念略显抽象,现在要讨论汇编的实现,当然就更抽象,所以在看这节之前,先确保自己对C语言指针和数组的基本概念的明确是很重要的。
汇编语言没有指针和数组的概念,汇编某种程度上可以看成是寄存器语言,而c更像虚存语言。寄存器里面存的值,代表的是地址还是常数值,CPU在执行汇编时本身是不清楚的,这些都由编译器根据上层语言来确定的。某个空间存的值,不能确定是地址还是常数,人理解起来当然觉得抽象,因此C才有了指针变量这种特殊的对象;同理,内存空间地址的随意跳变和间接引用取值也会让人深感心里无底,因此C又有了数组这种类似“变量串”的对象。
下面是一个数组引用的例子和对应的汇编代码,分析完此例,数组的汇编实现应该就比较清楚了。
int decimal(int *x)
{
int i;
int val = 0;
for (i = 0; i < 5; i++)
val = (10 * val) + *(&x[i]+i); //先别管程序执行的目的,因为编译器也不知道你想干嘛
return val;
}
00000000 <decimal5>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 53 push %ebx
4: 8b 5d 08 mov 0x8(%ebp),%ebx
7: b8 00 00 00 00 mov $0x0,%eax
c: b9 00 00 00 00 mov $0x0,%ecx
11: 8d 14 80 lea (%eax,%eax,4),%edx
14: 8b 04 cb mov (%ebx,%ecx,8),%eax
17: 8d 04 50 lea (%eax,%edx,2),%eax
1a: 41 inc %ecx
1b: 83 f9 04 cmp $0x4,%ecx
1e: 7e f1 jle 11 <decimal+0x11>
20: 5b pop %ebx
21: c9 leave
22: c3 ret
程序计数器3的压栈是实现“被调用者保存”寄存器ebx,接下来计数器4,获取参数x的值,传送给ebx,注意x是指针,里面存了整型变量的地址,因此这个地址值就顺利的被ebx保存鸟,既然都存地址,因此指针变量x和%ebx可以看成是等价的了(如果把mov替换成lea,那ebx得到的可就是参数x本身的地址了,而不是它存的地址O(∩_∩)O~)。
计数器7和c明显是初始化局部变量,eax和ecx明显是“调用者保存”寄存器,所以decimal作为被调用者就随便用啦,从后面的语句就能得知eax对应val,ecx对应i,可千万别仅仅依赖顺序哦!计数器11的操作很奇妙,实际运算效果是%eax+4*%eax=5*%eax,将5*val赋值给edx,不管先往下走,计数器14,%ebx+8*%ecx,就是*(x+8*i)赋值给eax,加*是因为这里是mov了不是上面的lea!是不是很高端?为什么要把x移动8倍的i呢?这就要从*(&x[i]+i)说起。
&x[i]是某个int型变量的地址,由于它的这一特殊属性,使得做加法运算时,你必须按照int型的步进来计算。因此&x[i]+1就是下一个int型变量的地址,也就是&x[i+1],同理&x[i]+i就是&x[2i],而既然int型是4字节的步进,因此我们很轻易的得出x+2i*4 的结论。
好了,%eax现在存储了*(&x[i]+i); 下一步计数器17,计算的是%eax+2*edx,上面我们知道edx存的是5*val,很自然就推出%eax+2*edx计算的是*(&x[i]+i) + 10*val的值,并将其传送给eax保存。好了,计数器1a是让i做自加,1b和1e是循环的判断和跳转,代码的分析暂告一段落。
这里有个问题,为啥要先计算5*val,然后再计算10*val呢?编译器有个很明显的倾向就是,不到万不得已不会用乘法指令imul,因为早期的CPU处理乘法非常费时,而加法运算要快上很多倍,所以才会使用lea的性质代替乘法和加法得出数值,事实上现在的CPU做乘法的速度已经非常接近加法了,不过目前GCC还没有为此作出修改的动作。
上面的例子我们看到,编译器利用lea和mov很好的实现了数组的功能,不过注意这只是一级数组,多级数组的情况又如何呢?看下面的例子。
typedef int row34_t[3][4];
………
int decimal_m(row34_t *x)
{
return