从汇编角度分析C语言指针
分析在ubuntu18.04中进行。
C语言源代码 pointer.c
#include <stdio.h>
int main(void)
{
int a=1;
int *pa=&a;
int *pb=NULL;
pb=pa;
return 0;
}
使用指令:
gcc -m32 -S -o pointer.s pointer.c
生成32位汇编源程序 pointer.s如下:
.file "pointer.c"
.text #代码段
.globl main #全局标签
.type main, @function
main: #代码入口
.LFB0:
.cfi_startproc
leal 4(%esp), %ecx #leal:加载有效地址,这句话的意思是将R[esp]+4赋值给R[ecx] l代表操作32位
.cfi_def_cfa 1, 0
andl $-16, %esp #32位下-16的16进制码为0xFFFFFFF0,将堆栈与下一个最低的16字节边界对齐
#为了SIMD(Single Instruction Multiple Data)指令,我们的例程不包括,所以可能没必要
pushl -4(%ecx)
pushl %ebp
.cfi_escape 0x10,0x5,0x2,0x75,0
movl %esp, %ebp
pushl %ecx
.cfi_escape 0xf,0x3,0x75,0x7c,0x6
subl $20, %esp
call __x86.get_pc_thunk.ax
addl $_GLOBAL_OFFSET_TABLE_, %eax #关于_GLOBAL_OFFSET_TABLE_
#https://stackoverflow.com/questions/9685699/what-is-global-offset-table
movl %gs:20, %eax #参考http://www.cache.one/read/15313050
movl %eax, -12(%ebp)
xorl %eax, %eax #eax寄存器归零
movl $1, -24(%ebp) #应该对应语句 a=1 即变量a的存储地址为ebp-24,保存的值为1
leal -24(%ebp), %eax #把变量a的地址写入寄存器eax
movl %eax, -20(%ebp) #把变量a的地址保存在内存ebp-20中,ebp-20应该是指针变量pa的存储地址,保存的值为变量a的存储地址值
movl $0, -16(%ebp) #ebp-16应该是指针变量pb的存储地址,其值初始化为NULL,即0.
movl -20(%ebp), %eax #把指针变量pa中保存的变量a的地址值写到寄存器eax,执行完毕后eax中是变量a的地址值
movl %eax, -16(%ebp) #把变量a的地址值保存在内存ebp-16中,即指针变量pb的存储地址,存储的值为变量a的地址
movl $0, %eax #返回值
movl -12(%ebp), %edx
xorl %gs:20, %edx #Stack canaries, 堆栈金丝雀,校验作用
#参见 https://stackoverflow.com/questions/12234817/what-does-this-instruction-do-mov-gs0x14-eax
je .L3
call __stack_chk_fail_local
.L3:
addl $20, %esp #恢复
popl %ecx #恢复
.cfi_restore 1
.cfi_def_cfa 1, 0
popl %ebp #恢复
.cfi_restore 5
leal -4(%ecx), %esp #恢复
.cfi_def_cfa 4, 4
ret #返回
.cfi_endproc
.LFE0:
.size main, .-main
.section .text.__x86.get_pc_thunk.ax,"axG",@progbits,__x86.get_pc_thunk.ax,comdat
.globl __x86.get_pc_thunk.ax
.hidden __x86.get_pc_thunk.ax
.type __x86.get_pc_thunk.ax, @function
__x86.get_pc_thunk.ax:
.LFB1:
.cfi_startproc
movl (%esp), %eax
ret
.cfi_endproc
.LFE1:
.hidden __stack_chk_fail_local
.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits
在as汇编中,以.开头的语句为汇编命令(或称为伪指令、指示符) --《Linux内核完全注释》3.2.2.2节
使用到的各个寄存器介绍(参考Linux系统分析入门–简单汇编代码分析):
eax:累加器(Accumulator)
ebx:基地址寄存器(Base Register)
ecx:计数寄存器(Count)
edx:数据寄存器(Data Register)
ebp:堆栈基指针(Base Pointer)
esi和edi:变址寄存器(Index Register)
esp:堆栈顶指针(Stack Pointer)
intel堆栈从高地址增长到低地址。
80386的寄存器,共16个,8个通用(如前所述)、段寄存器(6个)、状态寄存器和指令寄存器。如下表所示。
前面的代码由于目前汇编水平有限并不能完全看懂。但是可以分析一下明显与C语言语句有对应关系的几个。从movl $1, -24(%ebp)到movl %eax, -16(%ebp) 的栈内存分布。
这是在执行movl $1, -24(%ebp)时的内存分布,设ebp=0x20,则esp=0x08,寄存器ecx中的值保存在地址0x1c中
在执行完语句movl $1, -24(%ebp)后,内存ebp-24=0x8处的值变为1,此处保存变量a的值。
在执行完语句leal -24(%ebp), %eax之后,eax中的值为ebp-24,也即变量a的存储地址,在我们的假设中即eax=0x08。
在执行完语句movl %eax, -20(%ebp),内存分布如下
在执行完movl $0, -16(%ebp)之后,内存分布如下图
在执行完movl -20(%ebp), %eax后,eax中的值变为内存地址ebp-20=0x0c中保存的值,也即指针变量pa的值,即eax=0x08。
执行完movl %eax, -16(%ebp)后,内存地址ebp-16=0x10中保存的值变为0x08,即指针变量pb的值变为0x08,也即指针变量pb指向了变量a,此时内存分布为
如果在源代码最后加入一句:
*pa=2;
最后汇编文件会加入语句:
movl -20(%ebp), %eax #从地址ebp-20中取出保存的值,即指针变量pa的值,并赋给eax寄存器
movl $2, (%eax) #将2赋值给eax寄存器的值表示的内存
按我们的假设,执行完第一句之后,eax的值为指针变量pa的保存的值,也即内存地址ebp-20=0x0c处内存的值,为0x08。
执行完第二句之后,内存地址0x08处保存的值变为2,也即变量a的值变为2。
如果最后直接加入
a=2;
则汇编文件会加入语句:
movl $2, -24(%ebp)
即直接将值赋给变量a。就是将2写入保存变量a的地址ebp-24=0x08。
从上面可以发现先定义的变量在低地址,也就是栈顶,后定义的变量在高地址,也就是栈底。
参考:
1.https://en.wikibooks.org/wiki/X86_Assembly/GNU_assembly_syntax
2.INTEL 80386 PROGRAMMER’S REFERENCE MANUAL 1986