[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, %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
,同时我们也完成了整个程序的分析。
- [XiyouLinux] 纳新题的更深层次探讨(1)
- ESP8266作TCP服务的更深层次探讨
- 更深层次的理解Context
- 更深层次的理解Context
- 更深层次的理解Context
- UICollectionView的更深层次的用法
- 如何发现更深层次的bug?
- 如何发现更深层次的bug?
- 对button更深层次的研究 详解
- CSS盒子模型的更深层次理解
- 坚持#第211天~对云计算及未来方向的更深层次理解
- 基于MFC的社团纳新抽题系统
- 更深层的反射
- AsynTask更深的理解
- 自然界的目的,更深层次的密码以及未知的灾难----读《三体》第一部第三节
- 模板2——顺序表的实现(现代写法的进一步解析,更深层次的深浅拷贝)
- 对viewstates的理解更深入了(1)
- 更深层次分析一道试题——指针内存分配
- Android插件化开发教程(二)
- oracle
- 一道唬人的数据库题
- word 由于本机的限制,该操作已被取消.请与管理联系
- 一个人的 Android 开发 泡在网上的日子 / 文 发表于2017-03-30 17:08 第3637次阅读 一个人,开发 3 编辑推荐:稀土掘金,这是一个针对技术开发者的一个应用,你可以在掘金上
- [XiyouLinux] 纳新题的更深层次探讨(1)
- RecycleView的简单使用
- CRC校验
- sql执行顺序
- 智能指针剖析&模拟
- TCP协议详解(理论篇)
- Android中JNI用法
- iOS开发 GCD一些常用的方法
- Tomcat配置虚拟路径使上传文件和服务器分离及上传文件