接着上篇文章,我们大致了解内存被分成几个部分之后,就实际实验一下吧,我拿前面说过的变量文章里的例子代码:
#include<stdio.h>
#define YES 1
//extern int extern_var ;
extern reall_extern_var ;
int golbal_var=3;
int golbao_var2;
int main()
{
//extern int extern_var ;
printf("extern var is:%d and its address is:%x\n",reall_extern_var,&reall_extern_var);
int auto_x=15,i=10;
//printf("%d, %d\n",&YES);
printf("%x\n",&auto_x);
static int static_var;
printf("%x\n",&static_var);
register int reg_var=0;
for(i=0; i<10; ++i){
reg_var++;
// printf("%d",®_var); 寄存器变量没有地址
}
return 0;
}
不同的是,这次在代码中用printf语句输出一些变量的地址,便于我们分析印证
首先,不急于看可执行文件的结果,我们到汇编层面看一下,我们写的要被编译器怎么处理,处理成什么样?
说明:因为要学习ios开发,目前的环境是mac 10.9,采用otool对可执行文件进行反汇编的,在linux下可以用objdump达到类似的效果,为什么要反汇编?直接gcc生成的汇编代码实在是太难以阅读了....
结果如下:这里先列出来的是代码段
命令:otool -tV <可执行文件> > ans 参数t表示text段,> ans 是把结果重定向到ans的文本文件中.
relearn:
(__TEXT,__text) section
_main:
0000000100000ea0 pushq %rbp
0000000100000ea1 movq %rsp, %rbp
0000000100000ea4 subq $0x20, %rsp
0000000100000ea8 leaq 0xbb(%rip), %rdi ## literal pool for: "extern var is:%d and its address is:%x
"
0000000100000eaf leaq _reall_extern_var(%rip), %rax
0000000100000eb6 movl $0x0, -0x4(%rbp)
0000000100000ebd movl (%rax), %esi
0000000100000ebf movq %rax, %rdx
0000000100000ec2 movb $0x0, %al
0000000100000ec4 callq 0x100000f48 ## symbol stub for: _printf
0000000100000ec9 leaq 0xc2(%rip), %rdi ## literal pool for: "%x
"
0000000100000ed0 leaq -0x8(%rbp), %rsi
0000000100000ed4 movl $0xf, -0x8(%rbp)
0000000100000edb movl $0xa, -0xc(%rbp)
0000000100000ee2 movl %eax, -0x14(%rbp)
0000000100000ee5 movb $0x0, %al
0000000100000ee7 callq 0x100000f48 ## symbol stub for: _printf
0000000100000eec leaq 0x9f(%rip), %rdi ## literal pool for: "%x
"
0000000100000ef3 leaq _main.static_var(%rip), %rsi
0000000100000efa movl %eax, -0x18(%rbp)
0000000100000efd movb $0x0, %al
0000000100000eff callq 0x100000f48 ## symbol stub for: _printf
0000000100000f04 movl $0x0, -0x10(%rbp)
0000000100000f0b movl $0x0, -0xc(%rbp)
0000000100000f12 movl %eax, -0x1c(%rbp)
0000000100000f15 cmpl $0xa, -0xc(%rbp)
0000000100000f1c jge 0x100000f3d
0000000100000f22 movl -0x10(%rbp), %eax
0000000100000f25 addl $0x1, %eax
0000000100000f2a movl %eax, -0x10(%rbp)
0000000100000f2d movl -0xc(%rbp), %eax
0000000100000f30 addl $0x1, %eax
0000000100000f35 movl %eax, -0xc(%rbp)
0000000100000f38 jmpq 0x100000f15
0000000100000f3d movl $0x0, %eax
0000000100000f42 addq $0x20, %rsp
0000000100000f46 popq %rbp
0000000100000f47 ret
这是AT & T风格的汇编,和很早在组成原理课上学的x86 汇编有一些区别,不过对我来说都是一样(那些语法早就忘记了)
不过慢慢研究,还是会弄明白的,为了不一开始就看着头晕,我们要弄明白一些符号的意思
%号开头的一搬表示的都是一个寄存器,exx表示是32位寄存器,rxx表示64位寄存器,$开始表示一个立即数,0x表示16进制....
第一行是 pushq %rbp, push是一个压栈指令,把rbp寄存器的值压入栈中,bp寄存器里保存的是帧指针,看下图:
图片来源:某博客
上一篇提到的内存中栈那部分是由一块块栈帧构成的,栈帧大小不固定,而第一句指令是把之前的的帧指针压入栈,为了我们写的程序完成后能够继续之前的程序
movq %rsp, %rbp, 注意和x86汇编不同的是,指令第一个操作数是源操作数,第二个数是目标操作数。sp是栈指针,是会动态变化的,我们push之后,sp的值就变化了
而bp一般是不变的,把sp的值给bp,是建立新的帧(函数帧)
第3行,subq $0x20, %rsp, 因为栈的生成方向从高地址到低地址,sp向下偏移32个字节,是为函数分配局部变量,而main中的局部变量,只有几个int,很明显不需要32个字节
这么大,这是为了内存对齐,我们知道对齐一般都是2的多少次方,虽然内存中是一个字节一个字节储存,但是将数据从内存转移到其它地方,却是一块一块(因为这样有效率),提高了转移速度,现在的cpu也是一次运算多个字节,提高运算速度和精度,我们所说的32位或者64位机器,指的就是字长,是cpu一次处理的数据的长度,32位就是4个字节,64位就是8个字节,通常也是一般寄存器的保存数据长度和数据总线的位数相等。
我本人的操作系统式64位,但这里是偏移了32位,不能说就是按照32位来对齐,我减少定义的数据数量,最后main什么局部变量都没有,还是偏移了16,那就是按照16字节来对齐的。
然后,为什么需要内存对齐,假设一个int存放在 0xFFFFFF01~0xFFFFF04四个字节,第一次读取0xFFFFFF00-0xFFFFFF03四个字节,取后3个字节,然后读0xFFFFFF04-0xFFFFFF07,取第一个字节,两次的数据进行合并,才能取到一个int,如果数据进行过对齐,那么只需要一个内存周期,就能取到所有数据,内存一般不能从任意位置读取的,
对某些特定类型数据必须从特定地址开始。