casyup.me@outlook.com

0%

other/pointAndRefrence

前言

最近看了<现代操作系统>, 从内存管理那一章中获得了一些东西: 分页和分段

其中分页的概念让我对内存的管理有了更加清晰的认知

什么是分页?

就是将内存分为一个个小的页面(以4k为例).

在将磁盘数据加载进内存时, 以页为单位, 而将内存中的数据换出到磁盘中时, 也以页为单位.

对此, 一个具体的虚拟地址可以分为两部分:

1
页号 + 地址

其中, 页号会被替换, 而地址会被保留:

1
真实的地址 + 地址

真实的地址存放到进程虚拟地址映射表中

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
i2 = 100;
*pi = 111;

汇编:

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 版本的就不行