「MoreThanJava」機器指令到彙編再到高級編程語言_台中搬家公司

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

  • 「MoreThanJava」 宣揚的是 「學習,不止 CODE」,本系列 Java 基礎教程是自己在結合各方面的知識之後,對 Java 基礎的一個總回顧,旨在 「幫助新朋友快速高質量的學習」
  • 當然 不論新老朋友 我相信您都可以 從中獲益。如果覺得 「不錯」 的朋友,歡迎 「關注 + 留言 + 分享」,文末有完整的獲取鏈接,您的支持是我前進的最大的動力!

Part 1. 機器指令

上一次 我們已經了解了 二進制和 CPU 的基本原理,知道了程序運行時,CPU 每秒數以億次、十億次、百億次地震蕩着時鐘,同步執行着微小的 「电子操作」,例如:從內存讀取一個字節的數據到 CPU 又或者判斷字節中的某一位是 0 還是 1

CPU 本身有一組 規定好的 可以執行的 「基本動作」(被稱為 機器指令):

  1. 讀取指令;2. 執行指令;3. 寫寄存器;

這幾乎就是 CPU 工作的全部了。 這些動作雖然每次只能執行一次,但是每秒可以執行數十億次,這個數量級的「小操作」累加成為一個大的「有用的操作」。

處理器所做的一切都是基於這些微小的操作!幸運的是,我們已經不再需要了解這些操作的詳細信息就可以編寫和使用各類程序。諸如 Java 這一類的 「高級語言」目的 就是 將這些微小的电子操作組織成由人類可讀的「程序語言」表示的大型有用單元

機器指令演示

一條 機器指令 一般由內存中的幾個字節組成,它們告訴 CPU 應該執行一個什麼樣的 「機器操作」(是取數據還是寫寄存器等..)。處理器依次查看 CPU 中的機器指令,並執行每一條。內存中的一組機器指令被稱為 「機器語言程序」,或稱為 「可執行程序」

下面我們來使用機器語言來演示一個控制燈泡亮度的機器語言程序。

先和硬件做好規定

假設燈泡由內存中的某一個程序控制,該程序能夠完全打開和關閉燈泡,可以使燈泡變亮或變暗,機器指令一個字節長度,並且與機器操作對應如下:

機器指令 機器操作
00000000 停止程序
00000001 完全打開燈泡
00000010 完全關閉燈泡
00000100 燈泡暗淡 10%
00001000 將燈泡照亮 10%
00010000 如果燈泡完全點亮,則跳過下一條說明
00100000 如果燈泡完全熄滅,請跳過下一條說明
01000000 轉到程序的開始(地址 0)

Demo 程序 && 演示

根據上方作出的規定,我們寫下如下的程序:(為了方便理解,我把對應的機器操作也寫在了後面,實際的程序只包含機器指令)

地址 機器指令 機器操作
0 00000001 完全打開燈泡
1 00000010 完全關閉燈泡
2 00000001 完全打開燈泡
3 00000100 燈泡暗淡10%
4 00000100 燈泡暗淡10%
5 00000000 停止程序

所以這樣的一段程序執行效果就如下圖:

您可以嘗試自己利用 01000000(跳轉到程序開始) 來改寫程序來達到讓「燈逐漸變亮又逐漸變暗」的目的。

小結

上面演示的程序 核心思想 是:

  • 機器語言程序是內存中一系列機器指令的集合;
  • 機器指令由一個或多個字節組成(在此示例中,僅一個字節);
  • 處理器一次運行一條機器指令的程序;
  • 所有的小機器操作加起來都是有用的;

在實際的 CPU 中,擁有更多的機器指令,而且更詳細,並且不同的 CPU,指令集是不同的。典型的 CPU 擁有一千或更多的機器指令。

Part 2. 彙編語言

  • 圖片來源:http://www.ruanyifeng.com/blog/2018/01/assembly-language-primer.html

機器語言太 “反人類”

我們已經可以開始寫一些程序使用了,但是使用 機器語言編寫代碼會十分辛苦,比如:

00000001 00000010 00000001
00000100 00000100 00000000

即使你剛看過你也會對這一段就在 上方的實例代碼 沒有什麼感知,這是因為機器語言是設計給機器的,人類記憶和使用起來就會顯得十分麻煩。

如此你就會感知到 上個世紀 的程序員使用 打孔卡片

使用 紙帶

甚至是 直接插拔線路 or 按下開關

是一件多麼硬核的事情…

如果你對它們如何工作以及多麼硬核感興趣,可以參考一下下方的鏈接:

  • 開發語言小傳之一:最早的編程語言——機器語言 – https://blog.csdn.net/killer080414/article/details/42219091
  • 50年前的登月程序和程序員有多硬核 – https://coolshell.cn/articles/19612.html、

再附帶一個寶藏網站(哥倫比亞大學出版的計算機歷史,非常詳細),有條件的同學 非常推薦 進去瀏覽一下:

  • http://www.columbia.edu/cu/computinghistory/index.html

彙編語言誕生

CPU 的指令都是 二進制 的,這顯然對於人類來說是 不可讀 的。為了解決二進制指令的可讀性問題,工程師將那些指令寫成了 八進制。二進制轉八進制是輕而易舉的,但是八進制的可讀性也不行。

很自然地,最後還是用文字表達,加法指令寫成 ADD。內存地址也不再直接引用,而是 用標籤 表示。

這樣的話,就多出一個步驟,要把這些文字指令翻譯成二進制,這個步驟就稱為 assembling,完成這個步驟的程序就叫做 assembler。它處理的文本,自然就叫做 aseembly code。標準化以後,稱為 assembly language,縮寫為 asm,中文譯為 彙編語言

  • 圖片來源:http://www.ruanyifeng.com/blog/2018/01/assembly-language-primer.html

理解彙編語言

每一種 CPU 的機器指令都是不一樣的,因此對應的彙編語言也不一樣。本文介紹的是目前最常見的 x86 彙編語言,即 Intel 公司的 CPU 使用的那一種。

寄存器

要學習彙編語言,首先必須了解兩個知識點:寄存器內存模型

先來看寄存器。CPU 本身只負責運算,不負責儲存數據。數據一般都儲存在內存之中,CPU 要用的時候就去內存讀寫數據。但是,CPU 的運算速度遠高於內存的讀寫速度,為了避免被拖慢,CPU 都自帶一級緩存和二級緩存。基本上,CPU 緩存可以看作是讀寫速度較快的內存。

但是,CPU 緩存還是不夠快,另外數據在緩存裏面的地址是不固定的,CPU 每次讀寫都要尋址也會拖慢速度。因此,除了緩存之外,CPU 還自帶了寄存器(register),用來儲存最常用的數據。也就是說,那些最頻繁讀寫的數據(比如循環變量),都會放在寄存器裏面,CPU 優先讀寫寄存器,再由寄存器跟內存交換數據。

寄存器不依靠地址區分數據,而依靠名稱。每一個寄存器都有自己的名稱,我們告訴 CPU 去具體的哪一個寄存器拿數據,這樣的速度是最快的。有人比喻寄存器是 CPU 的零級緩存。

寄存器的種類

早期的 x86 CPU 只有 8 個寄存器,而且每個都有不同的用途。現在的寄存器已經有 100 多個了,都變成通用寄存器,不特別指定用途了,但是早期寄存器的名字都被保存了下來。

  • EAX
  • EBX
  • ECX
  • EDX
  • EDI
  • ESI
  • EBP
  • ESP

上面這 8 個寄存器之中,前面七個都是通用的。ESP 寄存器有特定用途,保存當前 Stack 的地址(詳見下一節)。

我們常常看到 32 位 CPU、64 位 CPU 這樣的名稱,其實指的就是寄存器的大小。32 位 CPU 的寄存器大小就是 4 個字節。

內存模型:Heap(堆)

寄存器只能存放很少量的數據,大多數時候,CPU 要指揮寄存器,直接跟內存交換數據。所以,除了寄存器,還必須了解內存怎麼儲存數據。

程序運行的時候,操作系統會給它分配一段內存,用來儲存程序和運行產生的數據。這段內存有起始地址和結束地址,比如從 0x10000x8000,起始地址是較小的那個地址,結束地址是較大的那個地址。

程序運行過程中,對於動態的內存佔用請求(比如新建對象,或者使用 malloc 命令),系統就會從預先分配好的那段內存之中,劃出一部分給用戶,具體規則是從起始地址開始劃分(實際上,起始地址會有一段靜態數據,這裏忽略)。舉例來說,用戶要求得到 10 個字節內存,那麼從起始地址 0x1000 開始給他分配,一直分配到地址 0x100A,如果再要求得到 22 個字節,那麼就分配到 0x1020

這種因為用戶主動請求而劃分出來的內存區域,叫做 Heap(堆)。它由起始地址開始,從低位(地址)向高位(地址)增長。Heap 的一個重要特點就是不會自動消失,必須手動釋放,或者由垃圾回收機制來回收。

內存模型:Stack(棧)

除了 Heap 以外,其他的內存佔用叫做 Stack(棧)。簡單說,Stack 是由於 函數運行臨時佔用 的內存區域。

例如我們在執行一個叫 main 的函數時,會為它在內存裏面創建一個 ,用來保存所有 main 中使用的內部變量。main 函數執行結束后,該幀就會被回收,釋放所有的內部變量,不再佔用空間。

如果在 main 函數 內部調用了其他函數,例如 add_a_and_b 函數,那麼執行到這一行的時候,系統也會為 add_a_and_b 新建一個幀,用來儲存它的內部變量。也就是說,此時同時存在兩個幀:mainadd_a_and_b。一般來說,調用棧有多少層,就有多少幀。

等到 add_a_and_b 運行結束,它的幀就會被回收,系統會回到函數 main 剛才中斷執行的地方,繼續往下執行。通過這種機制,就實現了函數的 層層調用,並且 每一層都能使用自己的本地變量

我們可以把棧理解為一個下方密封,而上方打開的「桶」。

生成的新幀放入我們稱之為 「入棧」,而釋放幀我們稱之為 「出棧」棧的特點 就是,最晚入棧的幀最早出棧(因為最內層的函數調用,最先結束運行),這就叫做 “後進先出” 的數據結構。每一次函數執行結束,就自動釋放一個幀,所有函數執行結束,整個棧就都釋放了。

彙編語言演示

舉個簡單的例子,我們需要計算:

(1 + 4) * 2 + 3

我們按照 「後綴表示法」 進行一下轉換:

1,4,+,2,*,3,+

我們平常使用的方法是 「中綴表示法」,也就是把計算符號放中間,例如 1 + 3,後綴則是把符號放最後,例如 1, 3, +

這樣做的好處是沒有先乘除后加減的影響,也沒有括號,直接運算就行了。(例如 1, 3, +,先把 13 保存起來碰到 + 知道是加法則直接相加)

OK,我們從頭開始使用彙編語言來編寫一下程序,首先第一步:把 1 保存起來(放入寄存器):

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

MOV  1

之後是 4, +,那就直接加一下:

ADD 4

然後是 2, *,那就直接乘一下(SHL 是向左移動一位的意思,二進制中左移一個單位就相當於乘以 2,例如 01 表示 1,而 10 則表示 2):

SHL 0

最後是 3, +,再加一下:

ADD 3

完整程序如下:

MOV  1
ADD  4
SHL  0
ADD  3

這似乎看起來比 00001111 這樣的二進制要好上太多了!程序員們感動到落淚:

Part 3. 高級編程語言

擺脫了 二進制,我們有了更可讀的 彙編語言,但仍然十分繁瑣和複雜,每一條彙編指令代表一個基本操作,例如:「從內存 x 位置獲取一個数字並放入寄存器 A」、「將寄存器 A 中的数字添加到寄存器 B 的数字上」。這樣的編程風格既費時又容易出錯,並且一旦出錯還很難發現。

例如,我們來看一看 「1969 年阿波羅 11號登月計劃」 用來 防止登月艙計算機耗盡自身資源 的 BAILOUT 代碼:

POODOO    INHINT
    CA  Q
    TS  ALMCADR
 
    TC  BANKCALL
    CADR  VAC5STOR  # STORE ERASABLES FOR DEBUGGING PURPOSES.
 
    INDEX  ALMCADR
    CAF  0
ABORT2    TC  BORTENT
 
OCT77770  OCT  77770    # DONT MOVE
    CA  V37FLBIT  # IS AVERAGE G ON
    MASK  FLAGWRD7
    CCS  A
    TC  WHIMPER -1  # YES.  DONT DO POODOO.  DO BAILOUT.
 
    TC  DOWNFLAG
    ADRES  STATEFLG
 
    TC  DOWNFLAG
    ADRES  REINTFLG
 
    TC  DOWNFLAG
    ADRES  NODOFLAG
 
    TC  BANKCALL
    CADR  MR.KLEAN
    TC  WHIMPER
  • 出處:改變世界的代碼行 – https://www.infoq.cn/article/5CaYH8NbS6BmptWKRgkX

似乎不太容易讀的樣子…

阿波羅登月計劃的源代碼在 Github 上已經公開,有興趣的可以去下方鏈接膜拜一下(可以去感受一下當時程序員的工程能力):

  • https://github.com/chrislgarry/Apollo-11

另外附一下當時代碼的設計負責人 Margaret Heafield Hamilton(女程序員)和完成的堆起來跟人一樣高的代碼量:

第一個高級語言:FORTRAN

John Backus1950 年以一名科學程序員的身份加入 IBM 時,已經可以使用諸如 ADD 之類的助記詞代替数字代碼來編寫程序,也就是我們的彙編語言。這使編程變得容易一些,但是即使是一個簡單的程序也需要數十次操作,並且仍然很難找到錯誤。

巴克斯認為,應該有可能創建一種編程語言,使一系列計算可以用類似於數學符號的形式來表達。然後,使用特定的翻譯程序(以今天的術語來說是編譯器)可以將其轉換為計算機可以理解的数字代碼。

Backus 在 1953 年向他的經理提出了這個想法。他得到了預算,並被鼓勵僱用一個小團隊來測試該想法的可行性。三年後,該團隊發布了一本手冊,其中描述了 IBM Mathematical Formula Translating System(簡稱 FORTRAN)。不久之後, IBM 向 IBM 704 的用戶提供了第一個 FORTRAN 編譯器。

Backus 和他的團隊創造了世界上第一種高級編程語言。科學家和工程師將不再需要將其程序編寫為数字代碼或冗長的助記符

FORTRAN 代碼演示

下面演示計算並輸出 8 * 6 的代碼實例:

program VF0944
implicit none

integer a, b, c
a= 8
b= 6
c= a*b

print *, 'Hello World, a, b, c= ', a, b, c
end program VF0944

對比彙編代碼,是不是看上去要清晰(人類可讀)多了呢?

FORTRAN 的意義

FORTRAN 的問世在計算機史上具有划時代的意義,它使計算機語言從原始的低級彙編語言走出來,進入了更高的境界,使得 計算機語言不再是計算機專家的專利,使廣大的工程技術人員有了進行計算機編程的手段。

由此計算機更快地深入到了社會之中,它在工業部門中初露頭角,更是在火箭、導彈、人造地球衛星的設計中大顯身手,因此有人稱 FORTRAN 語言使計算機的工業應用成了可能,是推動第二次世界大戰以後西方工業經濟復蘇和進入第二次工業革命的無形力量,是 “看不見的蒸汽機”。

FORTRAN 后時代

FORTRAN 高級程序設計語言的出現孕育了計算機軟件業,繼其之後,計算機高級程序語言的開發進入到了一個蓬勃發展的時代。

1959

Grace Hopper 發明了第一個面向企業業務的編程語言,又稱 “面向商業的通用語言”,也常常簡稱 COBOL。

1964

美國達特茅斯學院約翰·凱梅尼和托馬斯·卡茨認為,像 FORTRAN 那樣的語言太過專業,編程非常困難。於是他們簡化了 FORTRAN,並設計出了更適合初學者的 BASIC 語言。

1970

尼古拉斯·沃斯非常痴迷於編程語言,他率先提出了結構化程序設計思想併發明了 Pascal 語言。

此外他還提出了 Wirth 定律,意為 “軟件變慢的速度比硬件變快的速度更快”,這讓摩爾定律變得充滿諷刺。之後的 Electron.js 也確實證明了這一點。

1972

丹尼斯·里奇在貝爾實驗室工作期間發明了 C 語言,開啟了現代程序語言的革命。之後,他又添加了段錯誤和其他一些幫助開發人員的實用功能,大大提升了編程效率。

除了 C 語言之外, 他和貝爾實驗室的同事還創造了偉大的 Unix 操作系統。

1980

Alan Kay 發明了面向對象的編程語言 Smalltalk,在 Smalltalk 中,一切皆對象。

1987

拉里·沃爾發明了 Perl 語言。

1983

Jean Ichbiah 發現 Ada Lovelace 的程序從未運行成功過,因此決定用她的名字創建一種語言,於是 Ada 語言誕生了。

1986

Brac Box 和 Tol Move 通過融合 C 語言和 Smalltalk 的特徵,發明了 Objective-C。但由於其語法晦澀,不太容易理解。

1983

Bjarne Stroustrup 在 C 語言的基礎上引入並擴充了面向對象的概念,發明了—種新的程序語言並將其命名為 C++。

C++ 大大提升了應用程序的編程效率。

1991

Guido van Rossum 討厭帶有大括號的編程語言,於是他參考 Monty Python 和 Flying Circus 語法,併發明了 Python。

1993

Roberto Ierusalimschy 和其朋友創造了一門巴西本地的腳本語言。在本地化過程中,由於一個小的錯誤使得索引從1開始,而不是0。這門語言就是 Lua。

1994

Rasmus Lerdorf 為他個人主頁的 CGI 腳本製作了一個模板引擎,用來統計他自己網站的訪問量。

這個文件被上傳到網上之後用它的人越來越多。後來又用 C 語言重新編寫,還添加了數據庫訪問功能。這門語言就是 PHP。

1995

松本行弘發明了 Ruby 語言。

1995

Brendan Eich 利用周末時間設計了一種語言,用於為世界各地的網頁瀏覽器提供支持,並最終推出了 Skynet。他最初去了 Netscape,並將這門語言命名為 LiveScript,後來在代碼審查期間 Java 逐漸開始風靡,因此他們決定將其改名為 JavaScript。

後來 Java 使其陷入了商標麻煩,於是 JavaScript 被更名為 ECMAScript。但是人們還是習慣稱之為 JavaScript。

1996

James Gosling 發明了 Java,這是 第一個真正意義上面向對象得編程語言,其中設計模式在實用主義中占統治地位。

More…

對於這一段計算機歷史感興趣的同學可以拜讀一下「IT 通史 12.2 節 – 高級計算機程序設計語言」的內容,在線預覽鏈接如下:

  • https://books.google.com.hk/books?id=ZrAol3RzcNkC&printsec=frontcover&hl=zh-CN#v=onepage&q&f=false

高級語言分類

CPU 終究只認識二進制指令,在我們發明高級語言之後,仍然無可避免的需要進行 「翻譯」 工作。按照翻譯方式的不同,我們又把高級語言分為了 「編譯型」「解釋型」

編譯型

編譯型專業解釋為:

使用 專門的編譯器,針對 特定的平台,將高級語言源代碼 一次性 的編譯成可被該平台硬件執行的機器碼,並包裝成該平台所能識別的可執行性程序的格式,並且只需要編譯一次,以後再也不用編譯。其實可以簡單理解成谷歌/ 百度翻譯,我們把要翻譯的文字全部放進去,一次翻譯,下次使用直接使用上一次翻譯好的結果。

  • 優點(較解釋型):執行效率高(有解釋器省去很多翻譯的過程)
  • 缺點(較解釋型):開發效率低(寫完所有的代碼才能檢查 bug,得多恐怖呀???)

解釋型

解釋型專業解釋為:

使用 專門的解釋器 對源程序逐行解釋成 特定平台 的機器碼並 立即執行,它不需要事先編譯,直接將代碼解釋稱機器碼直接運行,也就是說只要某一平台提供了相應的解釋器即可運行代碼。其實可以理解成同聲傳譯,我們需要翻譯的時候,找一個翻譯員,對方說一句翻譯員翻譯一句,下次翻譯還是需要一個翻譯員一句一句的翻譯。

  • 缺點(較編譯型):執行效率低(寫一次翻譯一次)
  • 優點(較編譯型):開發效率高(寫一行翻譯一行,錯了馬上就知道,媽媽再也不用擔心我找不到 bug 了)

半解釋半編譯的 Java

不同廠商、不同時間開發的 CPU 的指令集是不一樣的,這就是上方為什麼提到要使用 專門的解釋器,要用於 特定的平台 的原因。

所以 Java 為了實現 「一次編譯,到處運行」 的目的,採用了一種特別的方案:先 編譯與任何具體及其環境及操作系統環境無關的中間代碼(也就是 .class 字節碼文件),然後交由各個平台特定的 Java 解釋器(也就是 JVM)來負責 解釋 運行。

編程人員和計算機都無法直接讀懂字節碼文件,它必須由專用的 Java 解釋器來解釋執行,因此 Java 是一種在 編譯基礎上進行解釋運行 的語言。(Java 程序運行流程如下)

Java 解釋器 負責將字節碼文件翻譯成具體硬件環境和操作系統平台下的機器代碼,以便執行。因此 Java 程序不能直接運行在現有的操作系統平台上,它必須運行在被稱為 Java 虛擬機的軟件平台之上。

Java 虛擬機(JVM) 是運行 Java 程序的軟件環境(我們後面會詳細說到,這是學習 Java 繞不過的題),Java 解釋器是 Java 虛擬機的一部分。在運行 Java 程序時,首先會啟動 JVM,然後由它來負責解釋執行 Java 的字節碼程序,並且 Java 字節碼程序只能運行於 JVM 之上。這樣利用 JVM 就可以把 Java 字節碼程序和具體的硬件平台以及操作系統環境分隔開來,只要在不同的計算機上安裝了針對特定平台的 JVM,Java 程序就可以運行,而不用考慮當前具體的硬件平台及操作系統環境,也不用考慮字節碼文件是在何種平台上生成的。

JVM 把這種不同軟、硬件平台的具體差別隱藏起來,從而 實現了真正的二進制代碼級的跨平台移植。JVM 是 Java 平台架構的基礎,Java 的跨平台特性正是通過在 JVM 中運行 Java 程序實現的。Java 的這種運行機制可以通過下圖來說明:

Java 語言這種「一次編寫,到處運行」的方式,有效地解決了目前大多數高級程序設計語言需要針對不同系統來編譯產生不同機器代碼的問題,即硬件環境和操作平台的異構問題,大大降低了程序開發、維護和管理的開銷。

  • 提示: Java 程序通過 JVM 可以實現跨平台特性,但 JVM 是不跨平台的。也就是說,不同操作系統之上的 JVM 是不同的,Windows 平台之上的 JVM 不能用在 Linux 平台,反之亦然。

參考資料

  1. Introduction to Computer Science using Java | CHAPTER 4 – http://programmedlessons.org/Java9/chap04/ch04_01.html
  2. 彙編語言入門教程 – http://www.ruanyifeng.com/blog/2018/01/assembly-language-primer.html
  3. CPU 是怎麼認識代碼的? | 知乎@Zign – https://www.zhihu.com/question/348237008/answer/843382847
  4. 改變世界的代碼行 – https://www.infoq.cn/article/5CaYH8NbS6BmptWKRgkX
  5. The History of FORTRAN – https://www.obliquity.com/computer/fortran/history.html
  6. 《IT 通史》 | @李彥
  7. A Brief Totally Accurate History Of Programming Languages – https://medium.com/commitlog/a-brief-totally-accurate-history-of-programming-languages-cd93ec806124
  8. 編程語言分類 – https://www.cnblogs.com/nickchen121/p/10722720.html

往期精彩

  1. 「MoreThanJava」當大學選擇了計算機之後應該知道的
  2. 「MoreThanJava」計算機發展史—從織布機到IBM
  3. 「MoreThanJava」計算機系統概述
  4. 「MoreThanJava」一文了解二進制和CPU工作原理
  • 本文已收錄至我的 Github 程序員成長系列 【More Than Java】,學習,不止 Code,歡迎 star:https://github.com/wmyskxz/MoreThanJava
  • 個人公眾號 :wmyskxz,個人獨立域名博客:wmyskxz.com,堅持原創輸出,下方掃碼關注,2020,與您共同成長!

非常感謝各位人才能 看到這裏,如果覺得本篇文章寫得不錯,覺得 「我沒有三顆心臟」有點東西 的話,求點贊,求關注,求分享,求留言!

創作不易,各位的支持和認可,就是我創作的最大動力,我們下篇文章見!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

您可能也會喜歡…