【反汇编玩耍2】论指针和数组
在反汇编观察后,你会发现:指针变量就是块内存区域,里面存放的是地址,你可以通过这个地址访问其它内存。
数组就是块连续的内存区域,里面连续排列着同样size的内存,多维数组也是一样的。
上述很简单,就不贴代码赘述了。
但人们一般纠结这样一个问题:数组名 是不是 一种指针?
也就是说arr[]的这个arr是不是一种指针?
这个问题之前csdn论坛上讨论的热火朝天:
http://bbs.csdn.net/topics/380226723
http://blog.csdn.net/yby4769250/article/details/7294718#reply
一种看法就是把这个数组名当作一种特殊的指针来看待,特殊之处在于是常量,不能改变其指向的位置。
一种是透过C语言,从C语言编译之后的汇编,视角来看,认为数组名和指针在内存中完全不是一回事。
太认真考虑其定义和概念就接近于一种“玄学”,我这里就单纯讲反汇编角度来分析 数组名 和 指针在内存中的差别吧。
随便写的代码:
#include <stdio.h> void func(int *p) { *(p+1) = 2; } void main() { int v = 1; int *p = &v; int v2 = *p + v; int arr[100]; int v3 = arr[0] + v; int v4 = *(arr+2) + v2; func(arr); }
0000000000001149 <func>: #include <stdio.h> void func(int *p) { 1149: f3 0f 1e fa endbr64 114d: 55 push %rbp 114e: 48 89 e5 mov %rsp,%rbp 1151: 48 89 7d f8 mov %rdi,-0x8(%rbp) *(p+1) = 2; 1155: 48 8b 45 f8 mov -0x8(%rbp),%rax 1159: 48 83 c0 04 add $0x4,%rax 115d: c7 00 02 00 00 00 movl $0x2,(%rax) } 1163: 90 nop 1164: 5d pop %rbp 1165: c3 retq 0000000000001166 <main>: void main() { 1166: f3 0f 1e fa endbr64 116a: 55 push %rbp 116b: 48 89 e5 mov %rsp,%rbp 116e: 48 81 ec c0 01 00 00 sub $0x1c0,%rsp 1175: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 117c: 00 00 117e: 48 89 45 f8 mov %rax,-0x8(%rbp) 1182: 31 c0 xor %eax,%eax int v = 1; 1184: c7 85 48 fe ff ff 01 movl $0x1,-0x1b8(%rbp) 118b: 00 00 00 int *p = &v; 118e: 48 8d 85 48 fe ff ff lea -0x1b8(%rbp),%rax 1195: 48 89 85 58 fe ff ff mov %rax,-0x1a8(%rbp) int v2 = *p + v; 119c: 48 8b 85 58 fe ff ff mov -0x1a8(%rbp),%rax 11a3: 8b 10 mov (%rax),%edx 11a5: 8b 85 48 fe ff ff mov -0x1b8(%rbp),%eax 11ab: 01 d0 add %edx,%eax 11ad: 89 85 4c fe ff ff mov %eax,-0x1b4(%rbp) int arr[100]; int v3 = arr[0] + v; 11b3: 8b 95 60 fe ff ff mov -0x1a0(%rbp),%edx 11b9: 8b 85 48 fe ff ff mov -0x1b8(%rbp),%eax 11bf: 01 d0 add %edx,%eax 11c1: 89 85 50 fe ff ff mov %eax,-0x1b0(%rbp) int v4 = *(arr+2) + v2; 11c7: 8b 95 68 fe ff ff mov -0x198(%rbp),%edx 11cd: 8b 85 4c fe ff ff mov -0x1b4(%rbp),%eax 11d3: 01 d0 add %edx,%eax 11d5: 89 85 54 fe ff ff mov %eax,-0x1ac(%rbp) func(arr); 11db: 48 8d 85 60 fe ff ff lea -0x1a0(%rbp),%rax 11e2: 48 89 c7 mov %rax,%rdi 11e5: e8 5f ff ff ff callq 1149 <func> } 11ea: 90 nop 11eb: 48 8b 45 f8 mov -0x8(%rbp),%rax 11ef: 64 48 33 04 25 28 00 xor %fs:0x28,%rax 11f6: 00 00 11f8: 74 05 je 11ff <main+0x99> 11fa: e8 51 fe ff ff callq 1050 <__stack_chk_fail@plt> 11ff: c9 leaveq 1200: c3 retq 1201: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 1208: 00 00 00 120b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
在main函数里,
局部变量v,*p,v2,arr[100],v3,v4在栈中分别为:
-0x1b8(%rbp)、-0x1a8(%rbp)、-0x1b4(%rbp)、-0x1a0(%rbp)、-0x1b0(%rbp)、-0x1ac(%rbp)
int v = 1;
int *p = &v;
int v2 = *p + v;
对应为:
movl $0x1,-0x1b8(%rbp)
lea -0x1b8(%rbp),%rax
//将局部变量v的地址,存在%rax,其实-0x1b8(%rbp)就是寻址写法,相当于%rbp的值-0x1b8,
//如果是mov就是按-0x1b8(%rbp)的值去内存取对应的内容,
//如果是lea,则直接取%rbp的值-0x1b8这个值,也就是局部变量的地址
mov %rax,-0x1a8(%rbp) //此时%rax存的是v的地址值,这里mov %rax是直接取这个值,
//如果写成mov (%rax),就是去寻址,也就是v的值,而非v的地址
mov -0x1a8(%rbp),%rax
mov (%rax),%edx
mov -0x1b8(%rbp),%eax //这里用的是mov而非lea,也就是取*p(寻址后取内存值),而非p(地址值)
add %edx,%eax
mov %eax,-0x1b4(%rbp)
int arr[100];
int v3 = arr[0] + v;
int v4 = *(arr+2) + v2;
对应为
mov -0x1a0(%rbp),%edx //将arr[0]存入%edx
mov -0x1b8(%rbp),%eax //将v存入%eax
add %edx,%eax //%eax存arr[100]+v
mov %eax,-0x1b0(%rbp) //存入v3
mov -0x198(%rbp),%edx //将arr[2]的值存入%edx
mov -0x1b4(%rbp),%eax //。。。
add %edx,%eax //。。。
mov %eax,-0x1ac(%rbp) //。。。
可以看出,在汇编的概念里,只有寻址的概念,具体说也只有取地址,或依据地址去取内存值的概念,也无所谓数组或指针的区别……
当然数组是一块连续的地址区域,比如上面的代码,arr[0]就是-0x1a0(%rbp),arr[2]就是-0x198(%rbp),跳了8个字节,也就是两个int的大小(2023年12月补充,修改,说实话之前基本没搞懂)
上述是讲数组名 和 指针的差别?
下面要讲,
而当数组作为函数参数时,其实就是转化为指针来玩的。
先讲调用函数时的反汇编代码(这里取数组地址,然后压入栈再call func):
func(arr);
00811537 lea eax,[arr]
0081153D push eax
0081153E call func (0811226h)
00811543 add esp,4
再讲func函数里面对数组操作的反汇编代码:
上面的代码的函数func:
void func(int *p) { *(p+1) = 2; }
反汇编代码为:
void func(int *p) { 1149: f3 0f 1e fa endbr64 114d: 55 push %rbp 114e: 48 89 e5 mov %rsp,%rbp 1151: 48 89 7d f8 mov %rdi,-0x8(%rbp) *(p+1) = 2; 1155: 48 8b 45 f8 mov -0x8(%rbp),%rax 1159: 48 83 c0 04 add $0x4,%rax 115d: c7 00 02 00 00 00 movl $0x2,(%rax) } 1163: 90 nop 1164: 5d pop %rbp 1165: c3 retq
这里形参为指针,没什么异议。
改写一下,形参改为,arr[]的话:
void func(int arr[]) { arr[1] = 2; }
void func(int arr[]) { 1149: f3 0f 1e fa endbr64 114d: 55 push %rbp 114e: 48 89 e5 mov %rsp,%rbp 1151: 48 89 7d f8 mov %rdi,-0x8(%rbp) arr[1] = 2; 1155: 48 8b 45 f8 mov -0x8(%rbp),%rax 1159: 48 83 c0 04 add $0x4,%rax 115d: c7 00 02 00 00 00 movl $0x2,(%rax) } 1163: 90 nop 1164: 5d pop %rbp 1165: c3 retq
可以看出二者(不管形参写成*p还是arr[])是没差别的。
数组作为函数参数,都是会被转化为指针来操作。
为何?
当然没有差别!因为入参是函数调用前处理好的,为这几句:
lea -0x1a0(%rbp),%rax
mov %rax,%rdi
callq 1149 <func>
先取数组地址,放在%rax,再存在被调用函数需保存的寄存器,%rdi中,也就是通过%rdi传参,传的就是数组地址,也可以理解为C语言的指针(2023年12月补充,修改,说实话之前基本没搞懂)
调用函数,开辟新的栈空间以后,都是通过这个%rdi获取数组地址(指针),所以是没有任何差别的
至於各個寄存器的作用,哪些是被調用者保存寄存器,參考此圖,取自《深入理解計算機系統》
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】