烦恼一般都是想太多了。

0%

汇编中关于对于Pointer的一个描述

在 C/C++ 中来说,指针是最普通不过的东西,但是当想要与汇编中对应的指针这么一个概念对应起来,还需要花点心思。特别是对于多级指针的时候,会感觉更加的茫然。当然,这和对指针的理解本身就不深,也有点关系。

其实,站在汇编程序的角度,我们代码内的任何符号(变量名称)对它都是没有意义,其只会对内存进行操作。同时,我们预定义的任何值,都会被转换成内部在数据段内的一个符号。

对于内存来说,一般会被内存进行分区,一部分存储代码(代码段),一部分存储的数据(数据段),代码在执行的时候,就是通过在数据段进行相关的数据获取,写入的。

所以在看代码的时候,放弃我们已经固有的观念吧,把变量,指针什么的全部忘记,全部看内存地址就行了。

最简单的例子

一个最简单的 C 例子,

// t.c
int main() {
int a = 1;
return 0;
}

利用 VS 的 cl 命令生成汇编代码:

cl /FAs t.c

结果:

PUBLIC	_main
; Function compile flags: /Odtp
; File d:\downloads\t.c
_TEXT SEGMENT
_a$ = -4 ; size = 4
_main PROC

; 2 : int main(){

push ebp
mov ebp, esp
push ecx

; 3 : int a = 1;

mov DWORD PTR _a$[ebp], 1

; 4 : return 0;

xor eax, eax

; 5 : }

mov esp, ebp
pop ebp
ret 0
_main ENDP
_TEXT ENDS
END

这里完全没有什么符号的概念的,这里就我们代码每一行转换为的汇编代码都做了一个转换。

栈的结构

基本上,每个函数调用都会看到这样的代码

push ebp
move ebp, esp

这两段代码什么意思呢?

首先,每程序(线程)在执行的时候,都会有一个线程栈,在 Windows中的线程 我们谈到,当我们建立一个线程内核对象的时候,其含有一个 CONTEXT 结构,这个结构保留了此线程上一次被调度执行后的寄存器信息。线程栈是在创建线程对象的时候就会建立的,所有函数在调用的时候,都依赖于这个栈来进行参数的传递或者返回,自动变量的分配等。

寄存器 ESP, EBP 就是通过在这个栈上的上下来回移动,来实现函数栈帧的分配和回收。

我们的栈一般来说,是从高地址,往低地址生长的,为什么需要这样干?这个就和函数调用的约定有关了,比如一个 cdecl 的函数,其最后一个参数会第一个入栈,第一个参数最后一个入栈,当我们传递的是不定数量的参数时,通过查看参数的地址是否到达栈顶,我们可以拿到所有的参数了。

push ebp 会将当前栈帧栈顶的值存入栈上,esp 自动往下移动 4 (32位)或 8字节(64位),mov ebp, esp 会让 ebp 指向当前栈帧的顶部。

事实上我发现, ebp 在指向当前栈帧的栈顶后,基本上就不会再变化了。

参数的传递

对于 cdel 约定的函数来说,其参数约定最后一个先入栈,第一个最后入栈,由调用者来清理栈的信息,我们可以通过例子来看一下。

// t.c
int add (int a, int b){
return a + b;
}

int main(){
int a = 1;
int b = 2;
return add(a,b);
}

其生成的汇编代码如下:


PUBLIC _add
PUBLIC _main
; Function compile flags: /Odtp
; File d:\downloads\t.c
_TEXT SEGMENT
_a$ = -8 ; size = 4
_b$ = -4 ; size = 4
_main PROC

; 5 : int main(){

push ebp
mov ebp, esp
sub esp, 8 ; 下移8个字节,为两个自动变量 a, b 预留内存空间

; 6 : int a = 1;

mov DWORD PTR _a$[ebp], 1 ; PTR 表明 EBP 内存储的是一个内存地址, DWORD 表明这个内存地址处值的类型是 DWORD 4字节, [EBP] 表示取寄存器内存储地址处的值

; 7 : int b = 2;

mov DWORD PTR _b$[ebp], 2; 同上

; 8 : return add(a,b);

mov eax, DWORD PTR _b$[ebp]
push eax ; 最后一个参数先入栈
mov ecx, DWORD PTR _a$[ebp]
push ecx ; 第一个参数最后入栈
call _add
add esp, 8 ;清理栈

; 9 : }

mov esp, ebp
pop ebp
ret 0
_main ENDP
_TEXT ENDS
; Function compile flags: /Odtp
; File d:\downloads\t.c
_TEXT SEGMENT
_a$ = 8 ; size = 4
_b$ = 12 ; size = 4
_add PROC

; 1 : int add (int a, int b){

push ebp
mov ebp, esp

; 2 : return a + b;

mov eax, DWORD PTR _a$[ebp] ; 直接从栈上取值,因为 ebp 入栈了,所以栈当前的内容为 :b, a, last ebp
add eax, DWORD PTR _b$[ebp]

; 3 : }

pop ebp
ret 0
_add ENDP
_TEXT ENDS
END

指针

void swap(int *a, int *b){
int temp = *a;
*b = *a;
*a = temp;
return;
}
int main(){
int a = 1;
int b = 2;
return swap(&a,&b);
}
PUBLIC	_swap
PUBLIC _main
; Function compile flags: /Odtp
; File d:\downloads\t.c
_TEXT SEGMENT
_a$ = -8 ; size = 4
_b$ = -4 ; size = 4
_main PROC

; 7 : int main(){

push ebp
mov ebp, esp
sub esp, 8

; 8 : int a = 1;

mov DWORD PTR _a$[ebp], 1

; 9 : int b = 2;

mov DWORD PTR _b$[ebp], 2

; 10 : return swap(&a,&b);

lea eax, DWORD PTR _b$[ebp] ; lea 取得 -4[ebp] 地址。为什么不直接传递 _b[ebp] 呢
push eax
lea ecx, DWORD PTR _a$[ebp]
push ecx
call _swap
add esp, 8
xor eax, eax

; 11 : }

mov esp, ebp
pop ebp
ret 0
_main ENDP
_TEXT ENDS
; Function compile flags: /Odtp
; File d:\downloads\t.c
_TEXT SEGMENT
_temp$ = -4 ; size = 4
_a$ = 8 ; size = 4
_b$ = 12 ; size = 4
_swap PROC

; 1 : void swap(int *a, int *b){

push ebp
mov ebp, esp
push ecx

; 2 : int temp = *a;

mov eax, DWORD PTR _a$[ebp]
mov ecx, DWORD PTR [eax]
mov DWORD PTR _temp$[ebp], ecx

; 3 : *b = *a;

mov edx, DWORD PTR _b$[ebp]
mov eax, DWORD PTR _a$[ebp]
mov ecx, DWORD PTR [eax]
mov DWORD PTR [edx], ecx

; 4 : *a = temp;

mov edx, DWORD PTR _a$[ebp]
mov eax, DWORD PTR _temp$[ebp]
mov DWORD PTR [edx], eax

; 5 : return;
; 6 : }

mov esp, ebp
pop ebp
ret 0
_swap ENDP
_TEXT ENDS
END

可以看到,指针的操作稍微麻烦一些,首先,从栈上某个位置处拿到参数的的值,然后将这个值当作地址对待,再次寻址,拿到真正的值。

mov	edx, DWORD PTR _b$[ebp] ; 先拿到 12[ebp] 处 四字节的内容
mov eax, DWORD PTR _a$[ebp] ; 拿到 8[ebp] 处 四字节的内容
mov ecx, DWORD PTR [eax] ; 间接寻址
mov DWORD PTR [edx], ecx ; 间接寻址

对于一个指针,需要读两次内存操作才能拿到具体的值。

  1. 拿到为指针分配的内存处的值。
  2. 将这个值作为内存地址再去去拿内容

C 中所谓的指针,在 汇编中是没有符号的,事实上叫指针也不确切,稍微绕一点的说法就是:

C 中的指针,在汇编中被表示为一个 存储 另外一个内存地址的 内存地址。

A pointer is a address its content is another address。