题目十一
以下程序段的运行结果是什么?
#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
,同时我们也完成了整个程序的分析。