题目十一
以下程序段的运行结果是什么?
#include<stdio.h> int main(int argc, char *argv[]) { int nums[5] = {2, 4, 6, 8, 10}; int *ptr = (int *)(&nums + 1); printf("%d, %d\n", *(nums + 1), *(ptr - 1)); return 0; }
为了一探究竟机器到底在执行该段程序做了什么,可以阅读该段代码对应的汇编指令,使用gcc -S指令(这里使用的gcc版本为7.1.1),可以生成类似于以下的汇编代码:
; 代码中略去了一些伪指令
.LC0:
.string "%d,%d\n"
.text
main:
.LFB0:
pushq %rbp ;将被调者保存信息压入栈
movq %rsp, %rbp ;将当前栈顶指针保存到%rbp中
subq $32, %rsp ;将栈顶指针减少32个字节
movl $2, -32(%rbp) ;存储nums[0],这里&nums[0]=%rbp-32
movl $4, -28(%rbp) ;存储nums[1],这里&nums[1]=%rbp-28
movl $6, -24(%rbp) ;存储nums[2],这里&nums[2]=%rbp-24
movl $8, -20(%rbp) ;存储nums[3],这里&nums[3]=%rbp-20
movl $10, -16(%rbp) ;存储nums[4],这里&nums[4]=%rbp-16
leaq -32(%rbp), %rax ;将%rbp-32的有效地址放入%rax中
addq $20, %rax ;给%rax+=20,此时%rax=%rbp-12
movq %rax, -8(%rbp) ;将%rax放入内存地址%rbp-8指向的内存中
movq -8(%rbp), %rax ;将%rbp-8指向内存的值放入%rax中
subq $4, %rax ;对%rax-=4
movl (%rax), %edx ;将%rax指向内存的值的低32位放入%edx(第三个参数)中
movl -28(%rbp), %eax ;将%rbp-28指向内存的值的低32位放入%eax中
movl %eax, %esi ;将%eax的值放入%esi(第二个参数)中
movl $.LC0, %edi ;将"%d,%d\n"放入%edi(第一个参数)中
movl $0, %eax ;将立即数0放入%eax(返回值)中
call printf ;调用printf函数
movl $0, %eax ;将立即数0放入%eax(返回值)中
leave ;恢复栈顶指针
ret
这段代码我们可以分以下几个部分来看:
首先,先使用了伪指令在内存中存储了printf的字符串字面量"%d,%d\n":
.LC0:
.string "%d,%d\n"
.text
接下来的部分就是我们的主函数了,我们先看代码的两端:
main:
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
; 此处略去了中间部分
movl $0, %eax
leave
ret
需要注意的是,这里leave指令等同于movq %rbp, %rsp与popq %rbp两条指令的合指令。
在进入main函数时,pushq将%rbp寄存器中被调用者的保存的信息压入栈,然后通过movq %rsp, %rbp将进入main函数之前栈顶指针的位置存储在%rbp寄存器当中,通过subq $32, %rsp将栈顶指针向上32字节,在栈中开出了足够的空间用于保存main函数中的各个变量。在中间代码过后,movl $0, %eax将立即数0作为main函数的返回值保存在约定存储返回值的%eax寄存器当中,这里由于规定main函数使用int类型的返回值,因此这里不使用%rax寄存器。最后leave指令将%rbp中存储的栈顶指针还原为进入main函数之前的状态,并把最初通过popq指令压入栈中的%rbp寄存器中的内容恢复原状。
然后我们开始阅读代码中的中间部分:
movl $2, -32(%rbp)
movl $4, -28(%rbp)
movl $6, -24(%rbp)
movl $8, -20(%rbp)
movl $10, -16(%rbp)
这段指令通过对比C代码可以发现是在存储nums数组中的变量,我们可以看到2作为数组的第一个元素被存在了%rbp - 32的内存地址上,其他地址以此类推。需要注意的是,栈指针的高地址代表栈底,低地址代表栈顶,而虽然这里称之为栈,但我们可以通过栈顶指针寻址,任意的访问其中的元素,并不像作为数据结构的栈只能访问栈顶元素。
接下来这段指令赋值并存储了指针ptr:
leaq -32(%rbp), %rax
addq $20, %rax
movq %rax, -8(%rbp)
首先第一个leaq指令将%rbp - 32指向的内存地址放入了寄存器%rax中,然后对寄存器%rax的值+20,而20字节正好是nums数组的大小,在加过20之后我们发现%rax的值等于%rbp - 12,正好指向了nums数组最后一个元素nums[4]之后的第一个int大小(4字节)的位置。这里我们发现,20是以一个立即数出现的,而在C语言中我们这里是&nums + 1,也就是编译器认为这里的+1与内存地址上+20等价,我们可以理解为是数组长度的+1。随后我们将%rax的值存储在%rbp - 8所指向的内存地址的位置。
接下来的这段指令计算并传递printf函数的三个参数:
movq -8(%rbp), %rax
subq $4, %rax
movl (%rax), %edx
movl -28(%rbp), %eax
movl %eax, %esi
movl $.LC0, %edi
第一条movq指令,从内存%rbp - 8位置中(此处存储了ptr指针)取出内容放入%rax寄存器中,随后subq指令,对%rax寄存器执行了%rax -= 4的操作,这里%rax的值本来应该是%rbp - 12,执行完毕之后应该为%rbp - 16,刚好就是nums[4]存储的位置,此时%rax寄存器中已经存储了算好的*(ptr - 1)的值,第三条movl指令%rax指向的内存的一个双字存储到%edx寄存器中,%dx系列的寄存器通常被作为存储第三个参数的寄存器,因此这里已经完成了第三个参数的传递。
之后movl指令直接将%rbp - 28的内存指向的一个双字存储到了%eax寄存器中,随后movl指令将%eax寄存器的内容复制到了%esi寄存器当中,%si系列的寄存器通常被作为存储第二个参数的寄存器,因此这里完成了第二个参数的传递。同时我们发现*(nums + 1)产生的指令与nums[1]相同,说明两者是等价的。最后的movl指令将字符串字面量作为参数复制到%edi寄存器当中,%di系列寄存器一般被当做存储第一个参数的寄存器,由此也完成了第一个参数的传递。
在这段指令中我们可以看出,gcc从右向左计算了参数,参数位置较后的参数*(ptr - 1)被第一个计算了出来。
最后,我们执行了函数printf:
call printf
在屏幕中打印出结果4,10,同时我们也完成了整个程序的分析。

本文通过分析一段C语言代码及其对应的汇编指令,详细解释了指针操作与数组访问的具体实现方式,帮助读者理解编译器如何处理指针运算。
1620

被折叠的 条评论
为什么被折叠?



