作者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/m.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/m.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