2019年1月13日 星期日

[自助日本行 4/6] 20180627 橫濱市 volks store, 港未來線

日期: 20180627
地點: 橫濱市 volks store, 港未來線
本文提到的時間均為日本時間, 沒寫單位的價錢為日幣。

我不會日文, 英文也不好: 文章中有關問路或是其他問人的資訊, 都是用很克難的方式完成 (簡單英文、簡單日文、比手畫腳)。

今天有點失眠, 凌晨 5 點多就醒了, 不太妙, 昨天明明很累的。

搭乘日本的火車對我來說是件困難的事情, 我打算用最簡單的方式來搭乘火車, 就是使用 suica card, 這樣我就不用花精神去搞定起點站和終點站的問題。而要去的橫濱剛好符合這樣的條件, 可以想成從高雄到台南, 並不需要搭乘自強號, 電聯車即可, 用一卡通這樣的方式。

今天打算到橫濱晃晃, 本來打算先從上野搭車到東京, 再由東京轉車道橫檳, 不過我在 JR 上野看到前往橫濱的路線 (京濱東北線) 之後就改變心意了, 直接搭到橫濱。

09:22 我到 JR 上野搭乘京濱東北線, 日本這種線很多, 對比台灣的話最主要有山線、海線、集集線之類, 京濱東北線有到橫濱, 我在 JR 上野月台看到, 就直接在該月台等車。

上車沒什麼問題, 意外的是竟然在「蒲田站」後, 所有人都下車了, 我以為這台是直達橫濱, 結果不是, 下車到對面月台轉車。

10:20 到橫濱, suica card 扣了 550, 不急著出站, 我繞了一下, 跑去看看「港未來線」要怎麼購票, 稍微玩了一下售票機之後, 沒問題, 和網路介紹的很類似, 應該知道怎麼購票, 這時才放心出站。



一出站迎面而來的風勢很大, 出乎意料, 放眼望去, 都是巨大的高樓。橫濱和我想像中不太一樣, 到處都是高樓大廈, 我以為會像是一個樸實的小鎮, 結果是個很現代化的城市。

早上預計去 volks 橫濱店, 紀錄在《[娃店] 横浜 volks store》。

出了 volks 橫濱店之後, 尋找中午的飲食, 意外看到了「一蘭拉麵」, 在 JR 上野車站也有一間「一蘭拉麵」, 通常要等很久, 我運氣不錯, 直接就有空位, 其實若要等, 我就不會吃了, 我是個非常厭惡排隊的人。



有名氣果然是好吃, 但沒好吃到要排隊, 在秋葉原的拉麵店中, 也是類似的口感, 根本就不用到一蘭排隊吃, 但是湯的鹹度很合適, 不會有日本拉麵過鹹的感受。



老規矩, 日本很常見到的票券機, 投錢進去, 換一張拉麵票券出來。我覺得這種機器其實並沒那麼好操作, 有時候看了好半天, 還是不知道怎麼按下那些按鍵。吃飽喝足之後, 該來去完成今天的預定行程。

下午的行程是搭乘「港未來線」, 我把購票方式全拍成以下的圖, 照著按應該是可以順利購得一日券。

港未來線 (みなとみらい線)
, 我的旅遊書寫 MM21, 害我老是查不到資料, 原來比較正式的名稱是港未來線 (みなとみらい線), 我還一直覺得奇怪, 為什麼用 MM21 去查, 都查到港未來線 (みなとみらい線)

一樣買了一日券, 460, 昨天搭路面電車, 今天搭地體, 各有不同樂趣。不過時間沒有昨天的一整天, 只有半天, 沒辦法逛比較多的站。

總共只有以下幾站:
橫濱、新高島、港未來、馬車道、日本大道(縣廳・大棧橋)、元町・中華街(山下公園)

並不算長, 其中好像也有其他線的地鐵會一起行駛, 不是很確定, 因為會看到不只是這些站的列車。

雖然不長, 但只有下午的時間我還是無法每個站都下去晃晃, 旅遊書上寫的好些點我都沒遇見, 會覺得可惜嗎? 到不會, 反正到日本來, 看任何地方都覺得有趣, 沒有一定要去哪裡不可, 自己覺得有感受到旅行的新鮮感即可。


fig 3 港未來線一日券購票流程

[馬車道]

我先去了馬車道, 這裡一樣有條商店街, 我在這裡花了太多時間, 這條商店街我覺得普通, 時間不是很多的話, 建議不要在這裡花太多時間, 而且我也不是來這買東西的。

fig 5
我忘記 fig5 是什麼建築了, 好像是什麼博物館之類的, 這種建築風格和台南的奇美博物館很類似, 我很喜歡這樣的建築, 在歐洲到處都見得到, 不過在台灣, 就沒這麼容易看見類似的建築物, 台灣很喜歡把老建築砍掉, 也不知道為了什麼, 以後可能都只能去國外看這些有特色的建築物了吧! 大概是鼓勵國人出國觀光, 台灣的觀光 ... 只是口號吧! 交通建設不行, 有文化歷史的東西也砍掉, 自然景觀也不好好保存, 台灣還剩下什麼可以吸引人, 難道只剩下那些交通很不方便的深山可看了嗎? 我們不能在交通便利的地方欣賞台灣的人文、歷史、古蹟、風景嗎?

在走了很長的一段路之後, 疲倦感漸漸來襲, 我回頭趕搭下一個站點, 不能來第一個點就回「上野」了吧!










[中華街]

中華街, 顧名思義, 賣的都是中國風味的東西, 小籠包、餃子 ... 一堆我很熟悉的食物, 所以我完全不會被這些東西吸引, 趕緊繞過這些店, 看看有沒有新鮮的東西, 其實我很猶豫要不要來這個站, 畢竟我對這些熟悉的東西完全不感興趣, 日本人要是來台灣看到賣小丸子的店家, 會有興趣進去逛嗎?

這裡有個横浜人形の家 (橫濱洋娃娃之家)可惜我錯過了, 我回台灣之後半年才知道。



但我還是發現了不錯的地方, 這是一間蛋咖啡店, 我不太喝咖啡、茶之類的飲品, 但我被這個吸引了過去, 這是用蛋來當作咖啡的一部分配料, 我雖然沒特別愛喝咖啡, 但對於這麼新奇的東西也是很好奇的, 也正好走累了, 進去喝杯蛋咖啡休息一下。



很好喝, 就這樣, 建議喜愛咖啡的朋友一定要來試試。

[日本大道]



來這是因為看起來可以靠近港口, 想說來看看海景, 但由於時間的關係, 在看到蔚藍的海天一線之後, 就急忙撤退, 因為我想去坐摩天輪。

[港未來]



我從「車馬道」出站時就看到這個摩天輪, 但不知道怎麼去, 後來才知道要從「港未來」出站, 才比較容易走到, 昨天我已經錯過一個摩天輪, 今天想說來補搭一下, 在我好不容易走到這裡時, 發現怎麼摩天輪沒在轉動, 奇怪, 遊樂園看起來有營業阿! 最後我才發現一個告示, 今天風大, 不啟動摩天輪, 哎呀! 我和摩天輪真的沒緣阿! 看來只好到高雄搭韓市長的愛情摩天輪 (20181104 國民黨韓國瑜當選高雄市長, 推「愛情摩天輪」政策)。



「港未來」出站後會在 Queen square 裡頭, 是購物中心, 很大一棟, 我走了很久才繞出來, 一個人的我對這種奢華的購物中心沒興趣, 我物質欲望很低, 不會想東買西買, 長長見識就夠了。

[回橫濱車站]

時間也差不多了, 我回到橫濱站時大約 18:00, 吃個晚餐再回上野好了, 大概要 50 分鐘, 餓肚子很慘的, 這邊很多百貨, 當然也有美食街, 我挑了親子丼飯, 總是要嚐嚐道地的日本料理才是, 看看日本的是不是比較好吃, 通常我得到的答案都是日本的比較好吃, 吃起來也安心, 台灣食安問題嚴重, 你都不知道吃了什麼東西進去。



這餐要價 1005 日圓, 在百貨公司算是一般行情, 既然都類似價錢, 我當然想花在日本的百貨美食街上。

回程時, 我搭了「東海道線」的火車回上野, 19:00 上車, 19:35 回上野, 這台的速度比早上的火車快很多, 一樣 550 日元, 用 suica 付款。

2019年1月4日 星期五

[2/5] 20181231 日本 gala 湯澤滑雪

日期: 20181231 (7) (2/5)
地點: 越後湯澤 - gala 湯澤 (ガーラ湯沢/ガーラゆざわ)
人數: 2

本篇文章提到的時間均為日本時間, 沒寫單位的金額為日圓。

我不會日文, 英文也不好: 文章中有關問路或是其他問人的資訊, 都是用很克難的方式完成 (簡單英文、簡單日文、比手畫腳)。

滑雪設備租借: 
SKI (スキー) (雙版滑雪板)
SKI BOOTS (卡在雙版滑雪板的鞋子)
SKI POLES (滑雪杖), 這個不用租借, 搭乘 Gondola (空中纜車, 類似貓纜) 上到滑雪場後可以免費租借。

另外一位也是租一樣的設備。
我自己在多租借了一個護目鏡。
這樣總共 6500 日圓。

gala 湯澤滑雪場門票: 3900 X 2


有些事情在你還不知道時很困難, 當你會了, 它就變得很簡單, 去「滑雪場滑雪」就是這樣 (注意: 不是滑雪本身)。身在台灣的我們, 不要說滑雪, 連雪是什麼樣子都很難見著, 所以不熟悉滑雪這件事情也是很容易理解的。而要去滑雪就得去國外, 日本、中國、韓國, 這幾個國家都可以滑雪, 這次選定了日本去滑雪, 來看怎麼克服這些難關。

難關一:
去哪裡滑雪? 如果你像我一樣什麼都不知道, 那就 gala 湯澤 (ガーラ湯沢/ガーラゆざわ) 吧! 這是最容易抵達的地方, 不過要學會搭新幹線才行。

從東京/上野可以搭乘「上越新幹線」, 查好時刻表 (我得先告知你, 這不是容易的事情, 謝謝某人的辛苦), 就出發吧!

我有買一本時刻表, 對照網路查的資料, 找起時刻, 方便多了。

請參考以下網站:
https://www.eki-net.com/pc/jreast-shinkansen-reservation/English/wb/common/timetable/e_joetsu_d/07.html

Total:79Page:7/10
NameMax
Toki
373
Max
Toki
325
Toki
375
Max
Toki
375
Toki
327
Toki
329
Toki
377
Max
Toki
377
Operation date*****
Tokyo14:1614:4014:5614:5615:1615:4015:5615:56
Ueno14:2214:4615:0215:0215:2215:4616:0216:02
Omiya14:4215:0615:2215:2215:4216:0616:2216:22
Kumagaya14:55|||||16:3716:37
Honjowaseda||||||||
Takasaki15:1115:3015:4615:46|16:3116:5316:53
Jomo-Kogen|15:46|||16:47||
Echigo-Yuzawa15:3716:0016:1216:12|17:0117:1917:19
GALA Yuzawa||||||||||||||||
Urasa|16:1216:2316:23|17:13||
Nagaoka15:5816:2516:3616:3616:4517:2617:3917:39
Tsubamesanjo16:0816:3516:4616:46|17:36||
Niigata16:2116:4716:5916:5917:0417:4817:5817:58

Tokyo 就東京嘛!
Echigo-Yuzawa 就是越後湯澤車站。

Max Toki 373 是新幹線車名。

我還不會買新幹線車票, 我們是買 jr pass - 廣域周遊券。

難關二:
購買滑雪場門票, gala 湯澤滑雪場的門票其實就是 lift (リフト) + Gondola (空中纜車, 類似貓纜) 的門票, lift (リフト) 是什麼呢?



這個就是 lift (リフト), 大家在電影應該都看過, 只是沒看過真正場景中的 lift (リフト)。

所以如果你買了票券, 但沒有去坐 lift (リフト), 有點浪費錢。就算不會滑雪, 還是建議去坐坐看, 頂多走路下來而已, 這個體驗很棒的。

搭乘 Gondola (空中纜車, 類似貓纜) 會驗票; 搭乘 lift (リフト) 也會驗票, 所以要把票券拿出來, 但是下山時搭乘 Gondola (空中纜車, 類似貓纜) 就不會驗票了。所以你可以 ... 黑嘿嘿

這個票價如果你有買廣域周遊券, 搭配這個 jr pass, 費用是 3500 日圓, 我是和民宿老闆娘買的門票, 3900 日圓, 但其實我有廣域周遊券, 沒搞清楚整個票券售價, 多花錢了。

我買的是一日券, 半日券自然便宜點。但是我建議第一次來滑雪的你, 買個一日券, 因為會花上很多時間在搞定這些滑雪裝備。


fig 3, 買票櫃台 fig 5 向民宿老闆娘買的門票, 需要對櫃台兌換

難關三:
租借設備, 需要的設備有: 滑雪用品, 滑雪服裝。

gala 湯澤滑雪場都有, 而且有會說中文的工作人員, 租借設備應該不會有問題。

gala 滑雪用具租借, 如果看這個網站對你來說有點困難, 那是很自然的, 我也是在滑了雪之後, 再看一次才理解這些內容。

但我不是在 gala 湯澤滑雪場租借這些設備, 我是在「青達雪具 Chenda Rental」租借的, 中文可以溝通, 所以沒什麼問題。

滑雪服裝我沒有租, 我穿的是 goretex 裝備, 從帽子、外套、手套、褲子、鞋子都是 goretex, 這身設備身價 25000 nt 以上, 租借的滑雪服裝不可能比我的高檔, 所以我省下租借服裝的費用, 而這身 goretex 也是我這幾天在越後湯澤的裝扮, 這裡可是冷的要死, 出動 goretex 也是很自然的。

除了服裝, 還有護具可以租借, 安全帽、護膝之類的 ...

滑雪設備我租的是:
SKI (スキー) (雙版滑雪板)
SKI BOOTS (卡在雙版滑雪板的鞋子)
SKI POLES (滑雪杖), 這個不用租借, 搭乘 Gondola (空中纜車, 類似貓纜) 上到滑雪場後可以免費租借。

另外一位也是租一樣的設備。
我自己在多租借了一個護目鏡。
這樣總共 6500 日圓。

另外還有 snow board, 雪盆這些滑雪用品。

最好帶個毛帽、口罩, 暴露在冰天雪地中, 很冷的。

[自身經驗]

我們從東京搭乘 Max Toki 325 (14:40) 到越後湯澤, 不過上車時間是 16:00, 對, 新幹線晚分了, 而且還晚很久, 臺鐵很常晚分, 台灣人很習慣的。

不知道是不是假日的關係, 東京車站人很多, 有多多呢? 就像下圖一樣的多, 大概擠了 20 多分鐘才抵達閘門口。



我們是住在越後湯澤 (えちごゆざわえき Echigo-yuzawa) 的 sansan 民宿, 老闆娘的老公就是「青達雪具 Chenda Rental」的老闆, 現在你知道為什麼我會在「青達雪具 Chenda Rental」這邊租滑雪設備了。

青達雪具 Chenda Rental」老闆派車到 sansan 民宿載我們到「青達雪具 Chenda Rental」, 租借滑雪設備之後, 再派車送我們到 gala 湯澤滑雪場, 夠義氣了吧!

如果你從越後湯澤車站過來, 只要走路 5 分鐘即可到達, 可以 fb, line, wechat 和老闆聯絡訊息。

gala 湯澤滑雪場門口搭電梯到二樓, 這裡好像就是 gala 湯澤車站出入口。

再來就是去換/買 gala 湯澤滑雪場的門票其實就是 lift (リフト) + Gondola (空中纜車, 類似貓纜) 的門票, 我租借了一個置物櫃 (1000 日圓), 二樓有分男置物櫃, 女置物櫃, 因為各自在男女廁所之中。三樓的置物櫃就沒分男女, 但我穿著滑雪的靴子, 不好走路, 不想去 3 樓。

然後搭乘 Gondola (空中纜車, 類似貓纜) 上去滑雪場, 就算你不滑雪, 搭這個纜車也值回票價了, 幾乎垂直的纜車加上外頭雪景, 美阿 ... 只是玻璃都蒙上大片霧氣, 看不太清楚外面的場景。



再來就是先去拿雪杖, 卡上 ski, 這個動作很難, 我花了十幾分鐘, 才讓滑雪靴子卡上 ski。再來就興奮的練習一下吧! 然後去搭 lift (リフト), 一路滑下來吧! lift (リフト) 有 2 邊, 難度高的上頭有警告標誌, 初學者不要來, 如果你沒注意, 請注意觀察小朋友都在哪裡搭乘 lift (リフト), 那個就是初學者專用的滑雪道。

那該怎麼從 gala 湯澤滑雪場回來呢? 有接駁車, 要搭乘黃色巴士, 到花月飯店 (松泉閣花月 kagetsu) 下車, 請注意要背起來花月飯店的日文發音 (kagetsu), 司機會用喊的目前到那個站或是那一個站有沒有人要下車, 沒聽懂可能會不知道要下車, 「青達雪具 Chenda Rental」就在花月飯店下車處的隔壁, 就是這麼簡單。請記得向工作人員索取時刻表, 巴士有藍色線、黃色線、紅色線 3 種, 可別搭錯。黃色巴士並不是黃色車身, 只是個巴士種類, 紅色、藍色亦然。

請查好滑雪場關門時間, 早點準備, 我們當天是 17:30 關門, 所以在 15:30 便搭 Gondola (空中纜車) 回到 gala 湯澤車站, 再去搭接駁車。

好了, 就是這樣, 希望這篇寫得夠詳細, 讓大家不用在參考其他資料就可以完成滑雪這個有趣的運動。

2018年12月21日 星期五

[code] 自己移動自己 - relocation

與智者同行, 你會不同凡響; 與高人為伍, 你能登上巔峰。
最近過得很舒服, 沒有在電腦技術上磨練, 看看半年來沒任何技術文章就知道了, 會跟不上快速更新的電腦世界嗎? 沒關係, 我研究的是老技術, 不是最新的技術潮流, 區塊騙, 不是是區塊鏈, 深度學習, 這些最近很紅, 通通沒再跟。

不過最新的東西有時候我還沒跟上, 它就過時了, 好險好險。

這次來談談 relocation, 對於 relocation 的概念疑惑很久了, 一直想找個方法來驗證, 知道概念很簡單, 要怎麼實作一個小程式來驗證就困難很多, 大概是 1:50 的難度。

在看過以下 2 篇有關 u-boot relocation 的文章之後, 再複習一下「程式設計師的自我修養 chapter 7」, 我終於有了實作的方向。
  1. uboot的relocation原理详细分析
  2. [uboot] (番外篇)uboot relocation介绍
建議先閱讀以上 2 篇文章, 因為以上 2 篇文章寫的內容我不會在重複介紹。

一開始思考要在 arm 還是 x86 寫這個測試程式, 畢竟是從 u-boot 借鏡而來 (u-boot 也有 x86 版本), 想了一陣子之後, 打算從 x86 來實作這個程式, 在 arm 上有些環境實在不好設置, 用 qemu 模擬不太有真實感, 用真實的開發板, 開發環境又很麻煩, 也不一定每個人都有一樣的開發板, 沒 ice/jtag 更麻煩, 所以最後還是選了 x86, 對每個人來說都是容易接觸的環境。而且 x86 和 arm 的 relocation 實作有點不同, 還好選了 x86, 我才能理解這些不同之處, 算是意外的收穫。

本來打算使用 bare-metal 方式, 但 legcy bios 512 byte 限制真的太麻煩, 最後這個程式超過 512 很多, uefi 我還沒能完全掌握, 改用 dos .com 來實作, 不怕, 之前都做過了, 不辛苦, 這是「累積」的力量, 程式本身大概花了 3 天。

期間出動了 qemu/bochs 這 2 個模擬神器, bochs 內建除錯器幫了很大的忙, 讓我找出程式的錯誤, 學會她是很有幫助的。其實我很不想靠這些工具, 希望靠自己的「冥想」可以想出問題點, 但我實在抵擋不了「時間」的誘惑, 用這些除錯工具, 我能在短時間就找到問題, 用「冥想」的話, 可能要超過一天才能想出問題吧!

疑! 不是說用模擬器會有不保險的問題嗎? 怎麼還是用了, 在 x86 上, 可以很容易把模擬器的環境在真實 PC 上測試, 沒問題的。

reloc.cpp 學習「uboot的relocation原理详细分析」這篇文章, L40 ~ 46 就是一模一樣的程式碼。

不過和 arm 的 machine code 不同, L41 test_val 不需要做 relocation 的處理。這讓我知道原來不同的 cpu, 是有不同的 relocation 情形, 而我也知道為什麼作者要加上 function pointer 的測試。最後我自己還加上 bss section 的測試, 該篇文章沒提到這個。

一開始的觀念很模糊, 不太好掌握, 什麼位址無關的程式碼, share object 之類的, 一堆和 relocate 相關的 elf section, 把我搞的一團亂, 我把這些東西都攪和在一起了, 後來選定目標, 就類似 u-boot, 把自己 relocalte 到新位址, 這麼做, 就比 share object 單純一些, 只要看 rel.dyn section 就可以。如果是要處理類似 share object 的東西, 就要看更多的 section。

要作到 relocation 需要:
  • cpu 支援
  • 編譯器支援
  • 自己還需要寫額外的程式來處理
由以上條件看來, relocation 相當不容易處理, 除了靠自己, 還得靠別人幫忙。

x86/arm 這麼流行的 cpu 當然符合第一、二個條件。

「程式設計師的自我修養」7.3.3 在說明這個, 不清楚這觀念的朋友可以參考這章節, 也會提到 GOT 這東西, 不過我的實做參考 u-boot 作法, 不使用 GOT, 只使用 rel.dyn section。

ld 加上 -pie, 就可以製作出一個可以 relocation 的執行檔。

ld -pie -m elf_i386 -static -Treloc.ld -nostdlib -o reloc.elf cpp_init.reloc.o reloc.o dos_io.o

-pie 是 ld 選項, 用來造出可 relocaltion 的執行檔, 當然並不是只加上這個選項, 你的執行檔就有能力 relocation (有這麼簡單就好了), 而是額外造出 .rel.dyn section, 讓程式碼在 relocation 時, 可以針對這些項目做修改, 進而達到 relocation 的能力。

Relocation section '.rel.dyn' at offset 0xa28 contains 28 entries:
 Offset     Info    Type            Sym.Value  Sym. Name

在移動自己的時候, 會需要這個資訊來修正對應的值。

-fPIC 則是在 share object 時會看到, 那在移動自己的時候, 需要使用 -fPIC 來編譯嗎? 你寫的程式載入到 0x100 可以執行, 載入到 0x500 也可以執行, 這就是 PIC, 和位址無關的程式碼。

如果你對於載入到 0x100, 0x500 覺得沒有差異的話, 表示 link 的概念還不熟。

測試使用了 -fno-pic + -pie 的組合, 發現還是會輸出 .rel.dyn section, 但是會多出很多項目, 猜測不需要 -fPIC 還是可以做到移動自己, 只是需要改變的項目會比較多, 這部份待以後有時間在研究。

本來我以為需要加入 -fPIC 的選項來輸出 PIC 的組合語言, 不過卻發現預設好像就是輸出 PIC 的組合語言, 不同的 gcc 版本, 預設的 option 不同。

-fPIC 會輸出 list 3 L1 這種程式, 令人看不懂的是 ebx 值是多少, 因為若不知道 ebx 的值, 就無法看懂這個組合語言, list 3 L2 之後, ebx 會是 2c3 (2c3 就是 list 3 L2 這行程式碼本身的位址), 相當於 arm 的 PC, 很神奇吧! 「程式設計師的自我修養」有說明這段 code, 就不重複了。

在 x86-64 之後, 可以直接使用 rip, 不需要在用這麼迂迴的作法, ref: x64下PIC的新寻址方式:RIP相对寻址, 有點類似 arm 的 pc。

list 3. -fPIC
1 2bd:   66 e8 bb 06 00 00       calll  97e <__x86.get_pc_thunk.bx>
2 2c3:   66 81 c3 1d 0a 00 00    add    $0xa1d,%ebx

編譯器已經做好他該做的事情, 剩下的該程式本身自己來了, 需要做的有:
  1. 複製程式本身到新位址
  2. 修改需要 relocation 的變數/函式
  3. 如何跳到新位址
大家猜猜看, 那個最難?

沒有相關經驗的程式員, 光第一點就會難倒人。因為這需要修改 linker script, 一般程式員幾乎是不會去修改這個的, 但這只是最簡單的第一關而已。

這次的範例程式大膽的使用了 c++, 甚至使用了 c++17 標準 (因為我用了 g++8), 但其實和 c 不會差太多, 因為 c++17 的很多特性我也不會, 有些地方麻煩了一點, 但已經難不倒我了。

reloc.cpp
  1 __asm__(".code16gcc\n");
  2 #include "io.h"
  3 #include "obj.h"
  4 
  5 typedef signed char s8;
  6 typedef signed short s16;
  7 typedef signed int s32;
  8 
  9 typedef unsigned char u8;
 10 typedef unsigned short u16;
 11 typedef unsigned int u32;
 12 
 13 extern "C" void jmp_to_reloc_addr();
 14 extern "C" u32 get_pc();
 15 
 16 #define R_386_RELATIVE 0x00000008
 17 
 18 #define BOCHS_MB __asm__ __volatile__("xchg %bx, %bx");
 19 
 20 extern int _start_ctors;
 21 extern int _end_ctors;
 22 
 23 void s16_print_int(int i, int radix);
 24 void print_str(const char   *s);
 25 
 26 void s32_memcpy(u8 *dest, const u8 *src, u32 n)
 27 {
 28   for (u32 i=0; i < n ; ++i)
 29     *dest++ = *src++;
 30 }
 31 
 32 void test_func(void)
 33 {
 34   print_str("test func\r\n");
 35   u32 v = get_pc();
 36   s16_print_int(v, 16);
 37   print_str("\r\n");
 38 }
 39 
 40 static auto test_func_val = test_func;
 41 static int test_val = 10;
 42 int data1;
 43 
 44 void (*data2)(void);
 45 
 46 int rel_dyn_test()
 47 {
 48   BOCHS_MB
 49   print_str("data_1: ");
 50   s16_print_int(data1, 16);
 51   print_str("\r\n");
 52   //data1 = 5;
 53   data2 = test_func;
 54   int i;
 55   i =  test_val;
 56   print_str("test_func_val: ");
 57   s16_print_int((int)test_func_val, 16);
 58   print_str("\r\n");
 59   (*test_func_val)();
 60     //printf("test = 0x%x\n", test_func);
 61     //printf("test_func = 0x%x\n", test_func_val);
 62   test_func();
 63   return i + data1;
 64 }
 65 
 66 extern int __image_copy_start;
 67 extern int __image_copy_end;
 68 extern int __rel_dyn_start;
 69 extern int __rel_dyn_end;
 70 extern u32 __bss_start__;
 71 extern u32 __bss_end__;
 72 
 73 void init_reloc_bss(u32 reloc_offset)
 74 {
 75   u32 reloc_bss_b = (int)&__bss_start__ + reloc_offset;
 76   u32 reloc_bss_e = (int)&__bss_end__ + reloc_offset;
 77   print_str("reloc_bss_b: ");
 78   s16_print_int(reloc_bss_b, 16);
 79   print_str("\r\n");
 80   print_str("reloc_bss_e: ");
 81   s16_print_int(reloc_bss_e, 16);
 82   print_str("\r\n");
 83   for (u32 b = reloc_bss_b ; b < reloc_bss_e ; b++)
 84   {
 85     *(u8*)b = 1;
 86   }
 87 }
 88 
 89 void reloc(u32 reloc_addr)
 90 {
 91   int from = (int)&__image_copy_start;
 92   int to = (int)&__image_copy_end;
 93   int image_size = to - from;
 94   u32 reloc_off = reloc_addr - from;
 95 
 96   print_str("reloc_addr: ");
 97   s16_print_int(reloc_addr, 16);
 98   print_str("\r\n");
 99 
100   print_str("reloc_off: ");
101   s16_print_int(reloc_off, 16);
102   print_str("\r\n");
103 
104 
105   int rel_dyn_from = (int)&__rel_dyn_start;
106   int rel_dyn_to = (int)&__rel_dyn_end;
107 
108   s16_print_int(from, 16);
109   print_str("\r\n");
110   s16_print_int(to, 16);
111   print_str("\r\n");
112   s16_print_int(rel_dyn_from, 16);
113   print_str("\r\n");
114   s16_print_int(rel_dyn_to, 16);
115   print_str("\r\n");
116 
117   s32_memcpy((u8*)reloc_addr, (u8*)from, image_size);
118   init_reloc_bss(reloc_off);
119   s16_print_int(image_size, 16);
120   print_str("\r\n");
121 
122   // modify rel.dyn section
123   for (int i = rel_dyn_from ; i < rel_dyn_to ; i+=8)
124   {
125     u32 v1 = *(u32*)i;
126     u32 v2 = *(u32*)(i+4);
127     #if 0
128     print_str("v1: ");
129     s16_print_int(v1, 16);
130     print_str("\r\n");
131 
132     print_str("v2: ");
133     s16_print_int(v2, 16);
134     print_str("\r\n");
135     #endif
136     if (v2 == R_386_RELATIVE)
137     {
138       u32 mem_data = *(u32*)(v1 + reloc_off); // 0xa2c
139       #if 0
140       print_str("mem_data: ");
141       s16_print_int(mem_data, 16);
142       print_str("\r\n");
143       #endif
144 
145       *(u32*)(v1+reloc_off) = mem_data + reloc_off; // locate offset
146       mem_data = *(u32*)(v1 + reloc_off);
147       #if 0
148       print_str("after reloc mem_data: ");
149       s16_print_int(mem_data, 16);
150       print_str("\r\n");
151       #endif
152     }
153     print_str("\r\n");
154   }
155 
156   //s16_print_int(rel_dyn_to, 16);
157   //print_str("\r\n");
158   //s16_print_int(v, 16);
159   //print_str("\r\n");
160 
161 }
162 
163 extern "C" int cpp_main(void)
164 {
165   print_str("cpp_main\r\n");
166   //s16_print_int(obj_count, 10);
167   rel_dyn_test();
168   u32 v = get_pc();
169   print_str("before reloc pc: ");
170   s16_print_int(v, 16);
171   print_str("\r\n");
172 
173   reloc(0x1100);
174   jmp_to_reloc_addr();
175 
176   print_str("after reloc to %cs:0x1100\r\n");
177 
178   v = get_pc();
179   print_str("after reloc pc: ");
180   s16_print_int(v, 16);
181   print_str("\r\n");
182 
183   rel_dyn_test();
184   
185   return 0;
186 }

[移動自己]

這需要靠 linker script 幫忙, reloc.ld L7, L24 將執行檔本身的 text, rodata, data section 的開始/結束位址記錄下來, 將他們複製到新的位址 0x1100 即可, 選擇移動到 cs:0x1100 也不是胡亂選的, 這是思考之後的決定, 最主要是除錯時比較方便。

reloc.cpp L117 的 s32_memcpy 就是在做這件事情。

reloc.ld
 1 /* for cb.c */
 2 ENTRY(_start);
 3 SECTIONS
 4 {
 5 
 6     . = 0x100;
 7     __image_copy_start = .;
 8     .text :
 9     {
10         *(.text)
11         *(.gnu.linkonce.t*)
12     }
13     .rodata :
14     {
15         *(.rodata*)
16         *(.gnu.linkonce.r*)
17     }
18 
19     .data :
20     {
21         *(.data.*)
22         *(.gnu.linkonce.d*)
23     }
24     __image_copy_end = .;
25 
26  . = ALIGN(4);
27 
28 __rel_dyn_start = .;
29  .rel.dyn : {
30   *(.rel.dyn*)
31  }
32 __rel_dyn_end = .;
33 
34     /* for g++ 4.7 */
35     .init_array :
36     {
37       __start_global_ctor__ = .;
38     }
39     __end_global_ctor__ = .;
40     .ctors :
41     {
42       start_ctors = .; _start_ctors = .; __start_ctors = .;
43       *(.ctor*)
44       end_ctors = .; _end_ctors = .; __end_ctors = .;
45 /*      . = ALIGN(0x1000); */
46      }
47      /*
48     .dtors :
49     {
50       start_dtors = .; _start_dtors = .; __start_dtors = .;
51       *(.dtor*)
52       end_dtors = .; _end_dtors = .; __end_dtors = .;
53       . = ALIGN(0x1000);
54      }
55      */
56 
57 
58     .bss :
59     {
60         sbss = .; __bss_start__ = .;
61         *(.bss)
62         ebss = .; __bss_end__ = .;
63         *(COMMON)
64         *(.gnu.linkonce.b*)
65     }
66 
67     /DISCARD/ :
68     {
69         *(.comment)
70         *(.eh_frame) /* discard this, unless you are implementing runtime support for C++ exceptions. */
71     }
72 }

簡單吧!

[修正 test_func_val]

list 2 L10, 11 是 test_val, L21, 22 是 test_func_val, 和 arm 的版本不同, x86 沒有 lable 這種東西, 直接就定位到 static 變數的位址, 所以處理 relocation 的方式也不同。

需要修改的只有 test_func_val, 因為 test_func_val 的值是 23d (list 2 L29,30), 是 test_func() 的位址, relocate 之後, test_func() 會變成 123d (我把整個程式從 cs:0x100 移動到 cs:0x1100), 所以 test_func_val (a48, list 2 L29) 的值要從 23d 改成 0x123d, 但是 test_func_val 也從 a48 移動到 1a48 了, 所以真正要改的位址是 1a48, 其內容要改成 0x123d, 很複雜吧! 知道理論和寫出程式難度還真的不同, 理論的話, 剛剛提到的細節都不用管, 只要知道 test_func_val 要做 relocation 的修改即可。

再來就是我怎麼會知道要改這個呢? 總不能每次都人工反組譯看吧, 所以編譯器很義氣的幫我們產生 rel.dyn section, 這樣就知道要改的就是這個地方。

list 2 L54 .rel.dyn section 不就告訴我們 a48 這個地方要調整嗎? 就是我上述說明的那樣修改。
修改方式: https://docs.oracle.com/cd/E23824_01/html/819-0690/chapter6-54839.html#chapter6-26
就是下表 Table 12-15, R_386_RELATIVE 的改法, 取 32bit 長度, 加上一個 base address。

32-bit x86: Relocation Types

The relocations that are listed in the following table are defined for 32–bit x86.
Table 12-15 32-bit x86: ELF Relocation Types

Name
Value
Field
Calculation
R_386_NONE
0
None
None
R_386_32
1
word32
S + A
R_386_PC32
2
word32
S + A - P
R_386_GOT32
3
word32
G + A
R_386_PLT32
4
word32
L + A - P
R_386_COPY
5
None
Refer to the explanation following this table.
R_386_GLOB_DAT
6
word32
S
R_386_JMP_SLOT
7
word32
S
R_386_RELATIVE
8
word32
B + A
R_386_GOTOFF
9
word32
S + A - GOT
R_386_GOTPC
10
word32
GOT + A - P
R_386_32PLT
11
word32
L + A
R_386_16
20
word16
S + A
R_386_PC16
21
word16
S + A - P
R_386_8
22
word8
S + A
R_386_PC8
23
word8
S + A - P
R_386_SIZE32
38
word32
Z + A



[bss section 的修改]
bss section 一樣被移動到 0x1100 的地方, 所以也需要修改其內容,

L72, L80 2c3+a05+54 = d1c
剛好是 data1 的位址

readelf -a reloc.elf
45: 00000d1c 4 OBJECT GLOBAL DEFAULT 14 data1
52: 00000d1c 0 NOTYPE GLOBAL DEFAULT 14 __bss_start__
57: 00000d24 0 NOTYPE GLOBAL DEFAULT 14 __bss_end__

data1 位於 bss section, 而 bss 落在 00000d1c - 00000d24, 8 byte 就是 data1, data2 佔的空間。
移動之後的 bss 會落在 1d1c ~ 1d24, 只要把這裡清成 0 即可。

[跳到 relocation 之後的位址]
這是個大問題, 也是理論派不會注意的地方, 因為這也和平台有關, arm 是這樣做, x86 又是另外的作法。我用了類似 __x86.get_pc_thunk.b 來移動到新的位址, 這是我自己想出來的作法, 不確定有沒有什麼比較正規的作法。

呼叫 jmp_to_reloc_addr() 之後會 jmp 到新的位址, 怎麼辦到的? jmp_to_reloc_addr() 結束之後, 會執行下一個位址也就是 reloc.cpp L176 print_str(), 我得讓他跳到新的 print_str() 而不是原來的那個 print_str(), 只要我可以得知這個位址, 加上 0x1100 - 0x100 就可以跳到新位址的 print_str(), jmp_to_reloc_addr() 就是在做這件事情。

list 2 的反組譯和 reloc.cpp 有點不同, 因為 reloc.cpp 我後來有再次修改, 反組譯的結果就不更新了。

list 2. objdump -m i8086 -CD reloc.elf
91 0000023d <test_func()>:
92  23d: 66 55                   push   %ebp
93  23f: 66 89 e5                mov    %esp,%ebp
94  242: 66 53                   push   %ebx
95  244: 66 83 ec 14             sub    $0x14,%esp
96  248: 66 e8 47 07 00 00       calll  995 <__x86.get_pc_thunk.bx>
97  24e: 66 81 c3 f2 08 00 00    add    $0x8f2,%ebx
98  255: 66 83 ec 0c             sub    $0xc,%esp
99  259: 67 66 8d 83 5c fe ff    lea    -0x1a4(%ebx),%eax
90  260: ff 
91  261: 66 50                   push   %eax
92  263: 66 e8 e5 04 00 00       calll  74e <print_str(char const*)>
93  269: 66 83 c4 10             add    $0x10,%esp
94  26d: 66 e8 d2 fe ff ff       calll  145 <get_pc>
95  273: 67 66 89 45 f4          mov    %eax,-0xc(%ebp)
96  278: 67 66 8b 45 f4          mov    -0xc(%ebp),%eax
97  27d: 66 83 ec 08             sub    $0x8,%esp
98  281: 66 6a 10                pushl  $0x10
99  284: 66 50                   push   %eax
90  286: 66 e8 96 06 00 00       calll  922 <s16_print_int(int, int)>

 1 000002b2 <rel_dyn_test()>:


71  2bd:   66 e8 2d 08 00 00       calll  af0 <__x86.get_pc_thunk.bx>
72  2c3:   66 81 c3 05 0a 00 00    add    $0xa05,%ebx
73  2ca:   87 db                   xchg   %bx,%bx
74  2cc:   66 83 ec 0c             sub    $0xc,%esp
75  2d0:   67 66 8d 83 3e fe ff    lea    -0x1c2(%ebx),%eax
76  2d7:   ff
77  2d8:   66 50                   push   %eax
78  2da:   66 e8 c9 05 00 00       calll  8a9 <print_str(char const*)>
79  2e0:   66 83 c4 10             add    $0x10,%esp
80  2e4:   67 66 8b 83 54 00 00    mov    0x54(%ebx),%eax // data1


 2  2b2:   66 55                   push   %ebp
 3  2b4:   66 89 e5                mov    %esp,%ebp
 4  2b7:   66 53                   push   %ebx
 5  2b9:   66 83 ec 14             sub    $0x14,%esp
 6  2bd:   66 e8 ab 06 00 00       calll  96e <__x86.get_pc_thunk.bx>
 7  2c3:   66 81 c3 45 08 00 00    add    $0x845,%ebx
 8  2ca:   87 db                   xchg   %bx,%bx
 9                                        
10                                        test_val
11  2cc:   67 66 8b 83 64 ff ff    mov    -0x9c(%ebx),%eax
12  2d3:   ff 
13  2d4:   67 66 89 45 f4          mov    %eax,-0xc(%ebp)
14  2d9:   66 83 ec 0c             sub    $0xc,%esp
15  2dd:   67 66 8d 83 7c fe ff    lea    -0x184(%ebx),%eax
16  2e4:   ff 
17  2e5:   66 50                   push   %eax
18  2e7:   66 e8 3a 04 00 00       calll  727 <print_str(char const*)>
19  2ed:   66 83 c4 10             add    $0x10,%esp
20                            
21                                        test_func_val
22  2f1:   67 66 8b 83 40 ff ff    mov    -0xc0(%ebx),%eax
23  2f8:   ff 
24  2f9:   66 83 ec 08             sub    $0x8,%esp
25  2fd:   66 6a 10                pushl  $0x10
26  300:   66 50                   push   %eax
27  302:   66 e8 f3 05 00 00       calll  8fb <s16_print_int(int, int)>
28 
29 00000a48 <test_func_val>:
30  a48:   3d 02 00                cmp    $0x2,%ax
31         ...
32 
33 
34 00000a6c <test_val>:
35  a6c:   0a 00                   or     (%bx,%si),%al
36         ...
37  
38 Disassembly of section .dynamic:
39  
40 00000a70 <_DYNAMIC>:     
41  a70:   04 00                   add    $0x0,%al
42  a72:   00 00                   add    %al,(%bx,%si)
43  a74:   20 0a                   and    %cl,(%bp,%si)
44  a76:   00 00                   add    %al,(%bx,%si)
45  a78:   f5                      cmc
46  a79:   fe                      (bad)
47 
48 descent@debian64:dos_cpp$ readelf -r reloc.elf
49 
50 Relocation section '.rel.dyn' at offset 0xb14 contains 3 entries:
51  Offset     Info    Type            Sym.Value  Sym. Name
52 0000014e  00000008 R_386_RELATIVE
53 00000154  00000008 R_386_RELATIVE
54 00000a48  00000008 R_386_RELATIVE

[relocate stack]
需要 relocate stack 嗎? u-boot 有 relocate stack 嗎? 我不知道, 但就算要做也不困難。好吧! 有點難, 和我想的不太一樣, 花了一天搞定, 嘴炮果然輕鬆多了。

本來使用 c++ function 來寫這功能, 一直有錯, 後來用了 bochs 除錯, 看了很多組合語言之後, 決定改用組合語言來寫這段 (因為 c++ 怎麼寫都寫不出來), 終於搞定。

list 3. qemu 執行結果
 1 Booting from Hard Disk...
 2 Boot failed: could not read the boot disk
 3 
 4 Booting from Floppy...
 5 Starting MS-DOS...
 6 
 7 A:\>reloc
 8 cpp_main
 9 data_1: 0
10 test_func_val: 26A
11 test func
12 2A0
13 test func
14 2A0
15 before reloc pc: 968
16 reloc_addr: 1100
17 reloc_off: 1000
18 100
19 DB4
20 E70
21 EB8
22 reloc_bss_b: 1EB8
23 reloc_bss_e: 1EC0
24 CB4
25 ---
26 yy from: 
27 yy to: 258A26AC
28 after reloc to %cs:0x1100
29 after reloc pc: 15F2
30 data_1: 1010101
31 test_func_val: 126A
32 test func
33 12A0
34 test func
35 12A0
36 
37 A:\>QEMU: Terminated





這個 relocation 的 dos 程式是用 g++ 8.2/c++17 開發的, 雖然用模擬器跑過了, 但不在真實機器上執行過一次, 就是不放心。

不過測試過程比我想的還難一點, 一開始找的 usb 隨身碟都無法正常開機, 突然想到我的 usb floppy, 好吧! 也可以, 剩下的 2 片 3.5 磁碟片, 只剩下一片是好的, 這是我最後一片可以用的 3.5 磁片了。

正常開啟 dos 時, 看到了久違的 A:\>, 懷念的感覺突然湧現, 當敲下鍵盤執行 reloc 時, 心中很緊張, 怕會執行錯誤, 好在結果是正確的, 程式正常的執行成功, 也印出預期的結果, 當然也正常的結束。

這個程式會在 cs:0x100 上執行, 然後將自己複製/移動到 cs:0x1100 並把 cs:0x100 的原本程式碼清成0, 再重新執行同一個函式。

bss/stack 也都做了 relocate 的動作, 最後在回到 dos。

dos 6.22 也能跟上 c++17 呢!

test func 在 relocate 之前印出 2A0, 在 relocate 之後印出 12A0, 同一個函式執行 2 次, 印出不同的 pc, 確認位址真的被移動到 0x1100 開始。事實上, 我做了更多的確認, 確認程式本身, bss, stack 真的都被移動到 offset 0x1000 (0x1100 - 0x100) 上, 全靠 bochs 的內建除錯器才完成。

這個技術有什麼功用呢? 說真的, 我還真不知道可以幹麻, 雖然 share object 有類似的東西, 但細節又有點不同, 也許可以當作處理 share object 的前哨站。看看能不能寫一個載入 share object 的程式, 也許下次可以挑戰看看。

u-boot 這麼做有他自己的理由, 但我實在想不出一般「正常」的程式用這招可以幹麻, 就 have fun 吧!

在完成這個程式之後, 我理解了部份 Dynamic section, 但為了不讓本篇更複雜, Dynamic section 補在最後面, 照理來說應該透過 Dynamic section 把 rel.dyn section 找出來, 而不是用 __rel_dyn_start, __rel_dyn_end 這個方式, 但這樣比較簡單, 也簡化整個程式的撰寫。

list 5 L12, 13, 14 就是用來找出 rel.dyn section, 大小是 24 byte, 每一個 entry 是 8 byte, 所以總共有 3 個 entry, rel.dyn section 位址在 0xe58, 就是 __rel_dyn_start 這個地方。

看來 relocation 還有很多相關的東西可以寫, 就留到來日吧!

list 5
 1 descent@debian64:dos_cpp$ readelf -d reloc.elf
 2 
 3 Dynamic section at offset 0xdb4 contains 15 entries:
 4   Tag        Type                         Name/Value
 5  0x00000004 (HASH)                       0xd64
 6  0x6ffffef5 (GNU_HASH)                   0xd74
 7  0x00000005 (STRTAB)                     0xd60
 8  0x00000006 (SYMTAB)                     0xd50
 9  0x0000000a (STRSZ)                      1 (bytes)
10  0x0000000b (SYMENT)                     16 (bytes)
11  0x00000015 (DEBUG)                      0x0
12  0x00000011 (REL)                        0xe58
13  0x00000012 (RELSZ)                      24 (bytes)
14  0x00000013 (RELENT)                     8 (bytes)
15  0x00000016 (TEXTREL)                    0x0
16  0x0000001e (FLAGS)                      TEXTREL
17  0x6ffffffb (FLAGS_1)                    Flags: PIE
18  0x6ffffffa (RELCOUNT)                   3
19  0x00000000 (NULL)                       0x0

寫這個程式時, 經常出動 readelf, objdump 等相關程式, 學會使用他們也是很重要的。