分析在ubuntu18.04中进行。
C语言源代码 pointer.c
#includeint 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



