作者LPH66 (かつて交わした约束)
看板C_and_CPP
标题Re: [问题] 关於结构内的指标
时间Thu Feb 9 07:26:15 2017
※ 引述《aresnmars (哎哟)》之铭言:
: 嗨,您好:
: 虽然您的签名档有注解,但我仍冒失地写站内信给您。
: 若您不介意,想和您简单地请教了。
不好意思, 容我回你的信到板上
因为你的问题其实底层有一个更基本的观念问题在里面
这个观念问题我觉得是很有可能其他人也会有的
(这也就是为什麽我的名片档会说有问题直接上板发问
这样除了其他人可以回答之外, 有一些东西也可以分享给其他有同样问题的人)
: 关於:
: LPH66: 那边讨论的就是当 sizeof(int) == sizeo(int*) 02/09 03:13
: LPH66: 且结构成员之间没有 padding 时的行为 02/09 03:13
: LPH66: 跟 nick5130 最一开始推文讲的东西一模一样 02/09 03:13
然後这里我要先更正一下我自己
我推文时只简单扫过他的回答说「p[1] 指向 s.p 指标」
就以为他讲的跟 nick5130 一开始提的是同一个状况
差在哪里等一下提, 但这正是所谓「未定义行为」所要表达的事
(而且他的回答似乎有些问题, 同样下述)
这里的基本观念就是:
对未定义行为追究原因是不切实际的
(讲一句不客气的话叫做: 天下本无事, 庸人自扰之)
你只要知道这里在 ptr[1] = 3; 这一行发生了未定义行为就足够了
(详细一点说, 这里的未定义行为是「对 ptr[1] 进行存取」)
还有一个更有趣的状况, 在那个环境里整支程式跑得完没有 crash!
同一支程式因为环境不同有了多种不同的结果,
这就是未定义行为
对写一般的程式而言, 未定义行为是必须极力避免的
除非你
非常非常确定你的环境会如何执行某些动作再说
只是这种状况在一般的程式里是很少见的
===========================================
拉一条分隔线, 下面要来拆解了; 但为了强调上面讲的观念所以再写一个警告:
※以下将会稍微详细分析这几行程式的可能行为
但不保证实际在哪个编译器编译或在哪台机器上执行就会有这些行为
未定义行为只代表编译器可以方便行事, 不表示哪些行为是必然的
以下的分析不完全理解也不会对写一般程式有什麽影响※
: 想请教您:
: - 结构之间的padding,指的是什麽?
: - 关於我找到的网页: goo.gl/vhP3td 内
: 描述道:
: - p指针占用8字节 (p指标占了8个bytes)
: - 修改了s.p指针高位的值
: - 修改了s.p指针的低位
: 如果sizeof(int) == sizeo(int*),那麽
: p指标不是占了4bytes吗?
: 高位、低位指的为何?
: (抱歉,我有点乱掉了。 希望您别介意回答我这样的问题)
: 先谢谢您的热心
上面提了未定义行为是在「对 ptr[1] 进行存取」
问题就在於到底对 ptr[1] 存取是存取到了什麽东西
警告里有说未定义行为代表编译器可以方便行事
也就是编译器可以不管违规时会怎样, 一律当做没有违规来看
ptr[1] 在没有违规时是会从 ptr 往後移动一个 int 大小, 从那里取值
因此在这种状况之下, 它存取到了什麽就随实际上那个位址里是什麽而定
回到这个结构:
struct S{
int i;
int *p;
};
这里有一个 int 变数跟一个指标变数
但这两个变数大小如何, 是如何配置在这个结构里, 标准并没有规定
(只有简单规定了它们之间的相对关系, 详细这里不提)
=== 状况一 ===
nick5130 一开始提的状况是这样的:
╭──────────── struct S s; ────────────╮
7000 7004 7008 7012
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│ int i; │ int *p; │
└───────────────┴───────────────┘
↑
方便行文起见令这个 byte 在位址 7000, 这也是 ptr 一开始指向之处
int 和指标都是 8 byte
於是 ptr 往後移一个 int 大小就是 7008, 正好取到了 p 的所在地
p[1] = 3; 就会写入一个 8 byte 的数值 3 进入位址 7008:
7000 7004 7008 7012
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│ 4 │ 3 │
s.p = ptr; 把占 8 byte 的位址 7000 放入 s.p 所在:
7000 7004 7008 7012
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│ 4 │ 7000 │
s.p[1] = 1; 取出 s.p, 往後移 1 个 int, 放入 1
这造成一个 8 byte 的数值 1 写入位址 7008:
7000 7004 7008 7012
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│ 4 │ 1 │
最後 s.p[0] = 2; 取出 s.p, 这里取到的是刚才写入时盖掉的 1 当做指标
也就是会把一个 8 byte 的数值 2 写入位址 1, 造成 crash
=== 状况二, 百度连结的说明 ===
那里所提的状况是这样的:
╭──────── struct S s; ────────╮
7000 7004 7008
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│ int i; │ int *p; │
└───────┴───────────────┘
int 是 4 byte, 指标是 8 byte
(这里就是我搞错的地方, 因为跟状况一同样地有 ptr+1 == &p
也就是「ptr[1] 指向 s.p 指标」
所以我把两个状况搞混了才会有那三行推文)
所以 ptr[1] = 3; 会把一个 4 byte 的数值写入 ptr+1 即位址 7004:
7000 7004 7008
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│ 4 │////// 1 ///// ??????????????│
注意到我後面四格打了问号, 表示那边的值没被动到, 还是未初始化的状况
s.p = ptr; 把占 8 byte 的位址 7000 放入 s.p:
7000 7004 7008
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│ 4 │ 7000 │
s.p[1] = 1; 取出 s.p, 往後移一个 int, 写入 1
即是把 4 byte 的数值 1 写入位址 7004:
7000 7004 7008
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│ 4 │////// 4 ////// **************│
右边 **** 的部份是代表那是「位址 7000」这个 8 byte 数值的後四 byte
这四 byte 的内容究竟是什麽也是跟环境有关的
这里有个专有名词叫 little-endian, 详细自行 google
这里我只提由此衍生的所谓「低位」「高位」这两个名词
它们命名的来源跟 little-endian 有关
而在这里它们分别指这个 8 byte 数值的前四 byte 和後四 byte
「修改了低位」就表示前四 byte 被修改了
这里就是我说他的回答有点问题的地方
因为 s.p=ptr; 并不是让 s.p 指向自己, 而是指向结构开头
回答的人误以为 s.p[1] 修改到的是 7008 开始的 4 byte
总之, 写进去的 1 跟被留下来的位址的「高位」部份组合成了一个不知指向何处的指标
因此 s.p[0] = 2; 把 4 byte 数值 2 存入这个未知位址, 造成 crash
=== 状况三, 神奇的不会 crash 的状况 ===
这个状况是这样的:
╭──────────── struct S s; ────────────╮
7000 7004 7008 7012
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│ int i; │<< padding >> │ int *p; │
└───────┴───────┴───────────────┘
int 还是 4 byte, 指标还是 8 byte
但两个成员之间有一块空间是谁都用不到的
这个即是所谓「结构成员之间的 padding」
关於为什麽会有 padding 这回事容我引一篇稍微不太正经的文章:
https://webptt.com/cn.aspx?n=bbs/Touhou/M.1337781212.A.3CB.html
(文章代码(AID):
#1FlElSFB (Touhou) [ptt.cc])
虽然不太正经但概念有到, 所以 XD
(如果要正经一点的解释, 专有名词是 alignment (对齐), 也请自行 google)
在这种状况下, ptr[1] = 3; 取出 ptr, 往後移到一个 int 到位址 7004
所以 4 byte 的数值 3 就写入这个地方
但它正好是 padding 所在的地方, 所以就变成这样:
7000 7004 7008 7012
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│ 4 │ 3 │??????????????????????????????│
s.p = ptr; 把位址 7000 放入 s.p 所在:
7000 7004 7008 7012
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│ 4 │ 3 │ 7000 │
s.p[1] = 1; 取出 s.p, 往後移一个 int, 写入 1
所以同样把 4 byte 的数值 1 写入位址 7004
7000 7004 7008 7012
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│ 4 │ 1 │ 7000 │
s.p[0] = 2; 取出 s.p, 写入 2 在那位置
7000 7004 7008 7012
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│ 2 │ 1 │ 7000 │
程式执行完毕, 没有 crash!
==========================
上面只是其中三种状况
这还是对编译器做了假设说对违规的东西当做没违规来编, 编成怎样不管
(甚至其实状况三还是有可能 crash
只要那块没人管的地方被写入时有被抓到就有可能)
除此之外还有一种我想得到的可能是当编译器强制插入边界检查时
这种状况之下 p[1] = 3; 会触发边界检查失败
至於是编译失败还是执行期 crash 则都有可能
这就跟结构的组织如何完全无关了
==========================
所以再讲一次: 以上这一大串分析不全了解也没差, 知道很可能不照你想的做事就好
未定义行为可能会炸可能不会炸, 会炸的方式也可能各有不同
除非你是做一些跟系统底层相关的东西不然没必要去全盘了解原因
以上
--
将很小又单纯的
命令《Code》组合成
函数《Function》。函数累积成更大更方便的
元件《
Parts》,成为
程式《App》。接着进行动态结合,相互通讯,打造出
服务《Service》。
李奥纳多知道,要得到结果,就必须持续进行非常单纯的作业。
为了展现出匹敌巨大建筑
的技术,现在非得将面前的碎片组合起来。
知道这条路多麽遥远的人,叫做
极客《Geek》。
将这份尊贵具体呈现的人,叫做
骇客《Hacker》。 --记录的地平线 Vol.9 p.299
--
※ 发信站: 批踢踢实业坊(ptt.cc), 来自: 180.177.29.238
※ 文章网址: https://webptt.com/cn.aspx?n=bbs/C_and_CPP/M.1486596378.A.3B5.html
1F:推 v86861062: :D 02/09 07:51
2F:推 Neisseria: 感谢大大解说 02/09 08:57
3F:→ aresnmars: 谢谢LPH大和Nick大的热心,虚心受教於您们的推文和来信 02/09 10:46
4F:推 ishokenmei: 解说详细 02/09 13:06
5F:推 ntucorner: 完全就是ptr1你根本不知道有甚麽 想要用那块记忆体就 02/09 15:57
6F:→ ntucorner: 要跟电脑说 直接用会出甚麽事没人知道 02/09 15:57
7F:推 cutekid: 大推(Y)^N 02/09 16:42
8F:推 nick5130: 这我只能用纸笔才画得出来 写成这样真是不容易 推 XD 02/09 16:43
9F:推 chuegou: 太清楚啦 02/09 19:15
10F:推 descent: 图文并茂 02/09 20:46
11F:推 james1022jk: 推一个 02/09 20:55
12F:推 BlazarArc: 推水晶球神 02/09 21:06
13F:推 ACMANIAC: 太用心了吧! 02/10 04:49
14F:推 pili100: 太详细了!推一个 02/10 11:27
15F:推 tuyutd0505: 推水晶球大神 02/10 16:15
16F:推 Ommm5566: 先推 02/10 16:39
17F:推 b98901056: 推猛猛的 02/10 17:18
18F:推 npes89033: 感谢大大解说! 02/10 17:31
19F:推 LamarcusAGG: 状况一的最後面为什麽s.p[0] = 2会把2写进位址1? 02/10 18:48
20F:→ LamarcusAGG: 有人可以解释一下吗感谢 02/10 18:48
21F:推 skygrango: 回楼上,根据内文s.p应当是占据7008~7016的指标 02/10 19:54
22F:→ skygrango: 而s.p[0]代表取s.p的内容也就是位址1,再将1当作指标 02/10 19:56
23F:→ skygrango: 抱歉,准确来说应该是7008~7015才对 02/10 20:01
24F:推 skygrango: 我有疑问的是,原文似乎是用gcc,没有强调64位元的gcc 02/10 20:12
25F:→ skygrango: 那指标应该也只占据4个byte而已才对 02/10 20:13
26F:推 yvb: 32位元通常类似状况一,只是8bytes=>4bytes,位址相对缩减. 02/10 20:25
27F:→ yvb: 而64位元,没特别指定编译参数,似乎比较偏向状况三. 02/10 20:28
28F:→ yvb: 若64位元gcc指定-fpack-struct之类,应该会是状况二, 02/10 20:31
29F:推 littleshan: 电脑硬体千百种,你怎麽会觉得世界上只分32和64? 02/10 20:34
30F:→ yvb: 但不知状况一要在何种64位元环境下会发生... 02/10 20:35
31F:→ yvb: 回littleshan大,当然不是只分32和64,但LPH66大回nick5130, 02/10 20:44
32F:→ yvb: 看来就是专指64bit.而上面skygrango推文应该是指32bit的情况. 02/10 20:46
33F:→ yvb: 若要再讨论其它位元的环境,可能要指定清楚是什麽架构系统, 02/10 20:47
34F:→ yvb: 那水晶球大大的文章恐怕补充不完了 :P 02/10 20:48
35F:→ LPH66: 简单回应上面, 状况一较接近 ILP64 ABI 02/12 23:49
36F:→ LPH66: 但大家口中的 "64-bit" 多是 LP64 ABI, 02/12 23:50
37F:→ LPH66: 所以会有 int 大小不同的状况 02/12 23:50
38F:→ LPH66: LP64 里一般来说状况三比较多, 但就如上面所说 02/12 23:51
39F:→ LPH66: 在指定 pack struct 时会变成状况二 02/12 23:51
40F:推 yvb: 问题就是不知哪找ILP64环境,或如何编译出ILP64程式(能run否?) 02/13 18:03
41F:推 FY4: 推推推 02/19 10:36
42F:推 VictorTom: 推:) 02/19 18:06
43F:推 mikukonn: 感谢大大 02/25 21:45
44F:推 GTX9487: 跪惹 03/27 13:02