前言
最近看了<现代操作系统>, 从内存管理那一章中获得了一些东西: 分页和分段
其中分页的概念让我对内存的管理有了更加清晰的认知
什么是分页?
就是将内存分为一个个小的页面(以4k为例).
在将磁盘数据加载进内存时, 以页为单位, 而将内存中的数据换出到磁盘中时, 也以页为单位.
对此, 一个具体的虚拟地址可以分为两部分:
其中, 页号会被替换, 而地址会被保留:
真实的地址存放到进程虚拟地址映射表中
emmm… 也就是说, 虚拟地址中, 有部分的地址是真实的
(如果我能知道那部分假的地址, 是否就有办法操作真实磁盘中的数据呢? 我突然有了不太好的想法 :) )
指针中是否会保留原始变量的地址?
指针中存放了数据, 而这样的数据能够找到指针所指向的变量
那么, 这样的数据是什么呢? 最直接的, 那么应该是地址, 考虑以下程序:
1 2 3 4 5 6 7 8 9
| 10 int main() { 11 int i = 1; 12 int *p = &i; 13 printf("%p\n", &i); 14 printf("%p\n", &p); 15 printf("%d\n", *((int *)&p)); 16 17 return 0; 18 }
|
15打印的是指针本身内存中所指向的东西, 将它与变量i的地址做以下对比, 会相同么? 以下是输出结果:
1 2 3
| 0x7fff12ca4efc 0x7fff12ca4ef0 315248380
|
i的地址是 0x7fff12ca4efc, 而p中保存的值(我将它解释为整数)是: 315248380
整型数字不怎么直观, 将它转为hex试试? => 0x12ca4efc
有没有觉得熟悉? 0x7fff12ca4efc —– 0x12ca4efc
除前面的 0x7fff, 后面的数字是一样的, 所以我们可以说指针保存了变量的地址, 但是并不准确
那么 0x7fff 就是那个页号么? 指针中只会存放真是地址? 好像不那么对…
emmm… 好像可以继续尝试, 因为当前环境是32位的, 这个地址数字明显超出了32位的表现范围
(我居然忽略了这一点 = =…)
或许我能获得更多的数据? 在代码中加入了下面一行:
1
| 16 printf("%d\n", ((int *)&p)[1]);
|
得到了数据: 32767 => 0x7fff
所以指针中直接保存了变量的地址(我们之前并没有拿到完全的数据, 地址超过了int的大小)
指针就是地址
emmm… 好像这个笔记不是那么有意义做了…
我是不是太慢了? 这些东西应该是初学者就可以去钻研的内容
(@btw: 为什么是[1] 而不是 [-1])
是否有办法知道哪些数据是与页号有关的?
emmm… 那些东西与内核有关, 我现在没有办法获得
其他的耦合知识: volatile
在书中我还读到了一个非常有趣的知识, 那就是进程表项有一个是否缓存标志位
这个标志位的意思是, 如果该位是1, 表示该页不被缓存
意思是什么呢? 如果要访问的数据是在该页中的, 那么访问时会去访问磁盘
而写入的时候, 也会直接往磁盘中写入, 因为内存中不缓存该页的数据
仔细想想和什么东西有关? 嗯, c++的 volatile 关键字
我打赌, volatile的实现一定与这东西有关(至少实现类似) 不过我现在的能力暂时无法证实
同时还有一个疑问, volatile 修饰一个变量, 而一个页是4k的, 如何将这两个东西分开呢?
为什么是[1] 而不是 [-1]
上面的例子中, 我继续访问数据, 使用的是 [1] 而不是 [-1]
我一开始是用 [-1], 因为栈是往下增长的, 之后发现 [1] 是正确的, 为什么?
的确实际上应该是 [1], 不然局部数组的访问就要乱套了
但是栈的确是向下增长的, 从 fc 和 f0 中可以看出来
那么底层到底对我的代码做了什么? 或许可以从汇编中得到答案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| 12 main: 13 .LFB1079: 14 .cfi_startproc 15 .cfi_personality 0x3,__gxx_personality_v0 16 pushq %rbp 17 .cfi_def_cfa_offset 16 18 .cfi_offset 6, -16 19 movq %rsp, %rbp // 保存了栈底指针 20 .cfi_def_cfa_register 6 21 subq $16, %rsp // 开拓栈帧, 16个字节 22 movl $1, -4(%rbp) // 嗯, 我们的数字1被放入内存了 23 leaq -4(%rbp), %rax // 将变量1的地址放入rax 24 movq %rax, -16(%rbp) // 将rax的值放入了栈... 栈顶?(因为这里是-16) 25 leaq -4(%rbp), %rax // 又将变量1的地址放入rax 26 movq %rax, %rsi 27 movl $.LC0, %edi // .LC0:.string "%p\n" 28 movl $0, %eax 29 call printf // rax存了变量1的地址, 这里应该对应: printf("%p\n", &i); 30 leaq -16(%rbp), %rax // 将栈顶的地址放入了rax 31 movq %rax, %rsi 32 movl $.LC0, %edi 33 movl $0, %eax 34 call printf // 现在rax是栈顶, 所以对应这一句: printf("%p\n", &p); 35 leaq -16(%rbp), %rax // 把p的地址放入rax 36 movl (%rax), %eax // 又移动到eax 37 movl %eax, %esi // 还移动 = = 38 movl $.LC1, %edi // .LC1:.string "%d\n" 39 movl $0, %eax 40 call printf // 对应: printf("%d\n", ((int *)&p)[0]); 41 leaq -16(%rbp), %rax 42 addq $4, %rax // *将rax+4, 也就是往栈顶移动的 43 movl (%rax), %eax 44 movl %eax, %esi 45 movl $.LC1, %edi 46 movl $0, %eax 47 call printf // 对应: printf("%d\n", ((int *)&p)[1]); 48 movl $0, %eax 49 leave 50 .cfi_def_cfa 7, 8 51 ret 52 .cfi_endproc
|
那么了解了, 虽然栈是往下移动的, 分配栈帧时也是往下移动的
但是 [1] 这种位移时, 是往上移动的, 也就是往高地址移动的
因为变量本身的地址开始是在栈的低地址, 是往上移动的
没有问题, 是正常的 :)
但是我又发现一个问题, 这里指针所占用的空间为8字节!
不过又想了一下, 这好像也正常的, 毕竟64位嘛…
引用呢? 引用又是什么样子的呢?
时隔几天, 突然想到了这个东西, 引用又是什么样子的呢? 它和指针实质的区别?
参考以下代码:
1 2 3 4 5 6 7 8 9 10
| int i = 100; int &i2 = i; int *pi = &i;
printf("%p\n", &i); printf("%p\n", &i2); printf("%p\n", &pi); printf("%d\n", i); printf("%d\n", i2); printf("%d\n", *pi);
|
emmm… 这很简单, 分别打印地址和值
我们来看看汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| main: .LFB1084: .cfi_startproc .cfi_personality 0x3,__gxx_personality_v0 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $32, %rsp // 32字节的栈帧 movl $100, -12(%rbp) // i leaq -12(%rbp), %rax // i 的地址放入了rax movq %rax, -8(%rbp) // 直接将 rax 的值放入了 i2, 也就是说, 引用也保存了变量的地址 leaq -12(%rbp), %rax // 这是一个完全可以优化掉的操作 movq %rax, -24(%rbp) // pi leaq -12(%rbp), %rax // 你家 rax 都没变过, 你怎么还来 (눈_눈) movq %rax, %rsi movl $.LC0, %edi movl $0, %eax call printf // &i movq -8(%rbp), %rax // 这里直接将栈中的值放进入了 movq %rax, %rsi movl $.LC0, %edi movl $0, %eax call printf // &i2 leaq -24(%rbp), %rax // 注意, 这是 lea movq %rax, %rsi movl $.LC0, %edi movl $0, %eax call printf // &pi movl -12(%rbp), %eax movl %eax, %esi movl $.LC1, %edi movl $0, %eax call printf // i movq -8(%rbp), %rax // 直接将地址放入 rax movl (%rax), %eax // 将这个地址中的值放入了(即 i 的地址) eax movl %eax, %esi movl $.LC1, %edi movl $0, %eax call printf // i2 movq -24(%rbp), %rax movl (%rax), %eax movl %eax, %esi movl $.LC1, %edi movl $0, %eax call printf // pi movl $0, %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc
|
引用和指针并无关键性的区别, 引用也会占用内存(废话 (눈_눈), 不过我记得培训时有个沙雕老师说不占)
当使用引用的值时, 它是像指针一样使用
而当对引用取地址时, 它是直接拿存储的数据, 而并非用存储的数据去寻址
(这应当是编译器的规定, 它这么编译了引用)
唯一不同是, 引用占了8字节, 这很合理, 但是为什么指针是12字节
(这是一个我在之前忽视了的点, 我曾看过那4字节中是什么, 结果是 0
(用指针的时候, 也用的是 movq, 这意味着只使用了 64 位, 即 8 字节, 为什么中空了 4 字节?)
或许我可以再试试赋值的时候, 引用和指针的不同之处
汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| main: .LFB1078: .cfi_startproc .cfi_personality 0x3,__gxx_personality_v0 pushq %rbp // 通过压栈的方式保存bp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp // 它没有显示说明栈帧有多大, 让我有点不习惯 ∑( ̄□ ̄;) .cfi_def_cfa_register 6 movl $2147483647, -20(%rbp) leaq -20(%rbp), %rax movq %rax, -16(%rbp) leaq -20(%rbp), %rax movq %rax, -8(%rbp) movq -16(%rbp), %rax movl $100, (%rax) // 它是用寄存器寻址的方式来赋值的 movq -8(%rbp), %rax movl $111, (%rax) movl $0, %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc
|
也就是说, 赋值是一样的, 嗯, 完全一样
指针和引用的安全性
还记得为什么引用比指针安全么? 因为对于引用是像值一样去使用它, 它仅仅是别名
(其实不是别名, 如你所见, 有些时候访问引用其实还是访问的是引用所占的内存)
它不会出现意外的 delete, 因为管理了它本身数据的访问, 也不会出现一些指针原有的错误(空指针, 野指针…)
我更倾向于: 引用是一个加了顶层const的非空, 不可用于delete的指针
summary
引用和指针的本质都是地址
题外话: 尝试篡改引用指向的对象
等等, 引用的内存也是在栈中的, 虽然 c++ 不让我用光明正大的方式修改它
但是, 既然是在栈中的数据, 那么, 我应该是可以改的, 那么就来试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int i = 100; int i2 = 200; int &ref_i = i;
long long *desc = (long long *)(&i) + 1;// 经过计算, i 的地址往上 8 个字节就是引用对象的内存 char arr[64]; sprintf(arr, "%ld", &i2); // 将 &i2 解释为ld类型数据, 放入数组中 string s(arr); // 构建string对象, 主要是为了能使用 stoll (눈_눈)
printf("%d\n", ref_i); // 100 *desc = stoll(s); // 现在, 它里面存储的数据是 i2 的地址了 printf("%d\n", ref_i); // 200 成功了, 它指向了 i2 ( ̄ˇ ̄) i2 = 300; printf("%d\n", ref_i); // 300 再次验证, 没错, 我们更改了引用指向的对象
|
汇编就不用看了, 因为这个程序就是根据自己脑补汇编中的样子来编写的
同理, 常量, 常量指针, 这些东西只要绕过编译器设的障碍就可以修改 (突然感受到了指针的魅力)
注: 经测试, 代码在 4.4 版本下的编译器可以, 而 4.8 版本的就不行