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

来源:互联网 发布:linux c va list 编辑:程序博客网 时间:2024/05/17 08:42

题目十一

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

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

原创粉丝点击