[XiyouLinux] 纳新题的更深层次探讨(1)

题目十一

以下程序段的运行结果是什么?


#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, %rsppopq %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,同时我们也完成了整个程序的分析。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值