My code works, I don’t know why.

國王的耳朵是驢耳朵

Ubuntu 14.04下使用dbdoclet 產生pdf手冊

| Comments

Sept/1/2015 更新:下面的方法有個問題,目錄的頁面會有從ii開始的羅馬數字頁碼。當known issue吧,猜測要從xsl修正,/usr/share/xml/docbook/stylesheet/docbook-xsl/fo/docbook.xsl應該有線索。

前言

Javadoc是一個掃描Java原始碼自動產生網頁文件的工具。除此之外,Javadoc還提供了API讓人實作doclet的功能。Javadoc把產出資料餵給doclet,doclet自行決定該怎麼處理,如

  • 轉成pdf文件
  • 轉成LaTeX文件
  • 轉成docbook文件

回到主題,為了要產生PDF手冊,並且需要符合下面的要求:

  • 封面
  • 版權宣告、版本變動紀錄
  • 目錄
  • SDK簡介,包含方塊圖
  • API使用方式
  • 範例

doclet 評估

目前找到幾個免費產生pdf的doclet,簡單的試玩結果如下

  • pdfdoclet
    • 直接輸出pdf,沒有目錄、針對手冊需求增加修改資料極度困難
  • AurigaDoclet
    • 和樓上的主要差別在多了目錄
  • ltxdoclet
    • 產出LaTeX檔案,可透過pdflatex輸出pdf。有亂碼,美觀程度不如上面兩個
  • TeXdoclet
    • 產出LaTeX,不支援pdflatex,直接放棄。
  • dbdoclet
    • 產出docbook,透過一些方式可以達到目標,雖然美觀程度還是比不上pdfdoclet和AurigaDoclet,但是比doxygen和ltxdoclet好,最後用這個產生手冊。
  • 其他族繁不及記載

    使用dbdoclet 產生pdf手冊步驟

詳細指令大家自行估狗,這邊單純提供key word。大略步驟如下:

  1. 透過任何方式(openoffice, LaTeX, etc)產生封面、版權宣告的pdf的檔案,假設為cover.pdf
  2. 撰寫針對你要產出手冊的XSL style sheet,我寫了三個東西。這邊要注意的是建議直接在XSL import docbook.xsl。
    • 人肉產生page break的指令,原因docbook不夠聰明,有些東西還是得人肉加page break。如一個段落之後就是範例程式的程式碼。這時候範例程式的程式碼放在頁面開頭的話,閱讀應該比較輕鬆。
    • header的描述
    • footer的描述,可塞入圖片
  3. 以docbook格式撰寫你要塞的內容如
    • 簡介
    • 範例程式
  4. 透過javadoc執行dbdoclet,我這邊跑完會產生Reference.xml
  5. 使用sed或是你順手的字串工具
    • 修改產出的Reference.xml,找對的地方include自己寫的內容
    • 如果有必要,修改針對你要產出手冊的XSL style sheet
  6. 透過xsltproc產生fo檔。幾點要注意的
    • 要加–xinclude,不然程式不會去include你自己寫的xml
    • /usr/share/xml/docbook/stylesheet/docbook-xsl/fo/docbook.xsl不用指定,因為你自己寫的XSL style sheet已經import了,這個問題卡了我一陣子時間。
  7. 使用fop將fo轉成pdf檔,假設為draft.pdf
  8. 使用pdftk
    • 把你的draft.pdf目錄前面頁面全部幹掉,另存新檔如temp.pdf
    • cover.pdftemp.pdf合併,就會有封面和含目錄以及你設定的手冊了。

Ubuntu 14.04下使用Doxygen 產生pdf手冊

| Comments

一個pdf程式SDK手冊,你會期待有

  • 封面
  • 版權宣告、版本變動紀錄
  • 目錄
  • SDK簡介,包含方塊圖
  • API使用方式
  • 範例

Doxygen是一個open source的文件產生工具,預設是產生HTML。不過它也可以產生很正規的文件,快速紀錄一下,細節還是要自行補完。要達到上面的目的另外前題是你要會估狗查詢使用LaTeX的用法。

  1. 安裝doxywizard
  2. 安裝doxygen-latex
  3. 執行doxywizard,透過GUI設定
    • 文件輸出目錄
    • 原始碼目錄
    • 開啟latex輸出
    • (需要塞範例程式) export -> Input -> EXAMPLE_PATH
  4. 細項設定完成後,存檔。
  5. 到輸出文件的latex目錄中,開啟refman.tex。剪貼
    • %--- Begin generated contents ---之前的文字,另存成header.tex
    • %--- End generated contents ---之後的文字,另存成end.tex
  6. 重新執行doxywizard 你剛才存的Doxyfile
    • expert -> LaTeX -> 打開
      • LATEX_HEADER -> 選你剛才存的header.tex
      • LATEX_FOOTER -> 選你剛才存的end.tex
  7. 存檔
  8. 修改header.tex,可以加入
    • 封面
    • 版權宣告
    • 你產生API文件前的任何東西如簡介,方塊圖,貓的照片等
    • 自訂的header/footer
  9. (需要塞範例程式)
    • 在程式碼註解塞入\example 範例程式檔名
    • 範例程式檔名存到前面EXAMPLE_PATH的路徑中
  10. doxygen 你剛才存的Doxyfile
  11. 到輸出文件的latex目錄中
    • make,目錄由程式自動產生
  12. 開啟refman.pdf,檢查輸出是不是你要的,不是的話回到第八步

雜記: What a C Programmer Should Know About Memory

| Comments

出處: What a C programmer should know about memory

這是筆記,不是導讀。單純放我覺得特別的東西和感想。不會自我要求可讀性和文章架構,請自行斟酌。

Understanding virtual memory - the plot thickens

  • The virtual memory allocator (VMA) may give you a memory it doesn’t have, all in a vain hope that you’re not going to use it. Just like banks today. 幹原來銀行是虛擬記憶體

  • malloc給valid pointer不要太高興,等你要開始用的時候搞不好OS給個OOM說人肉鹹鹹。簡單來說就是一張支票,能不能拿來開等到兌現才知道(煙)

Understanding stack allocation

原來C99的variable length array的運作是因為stack frame的特性,反正你要多少stack在橋的時候順便加一加。malloa一樣的原則。又學到了

When to bother with a custom allocator

  • 今日英文(?): GP allocator, General purpose allocator,你每天用的malloc是也。什麼,沒用malloc過啊,還真是幸福(煙)。

Slab allocator

  • 有的時候程式會allocte並使用多個不連續的記憶體區塊,如樹狀的資料結構。這時候對於系統來說有幾個問題,一是fragment、二是因為不連續,無法使用cache增快效能。

作者posix_memalign()可解決這類的問題。目前看範例,看不懂,感覺上和malloc很大的記憶體空間,自己管理差不多。先跳過,有大大路過可以留言解惑一下。

Demand paging explained

  • Linux系統提供一系列的記憶體管理API

    • 分配,釋放
    • 記憶體管理API
      • mlock,禁止被swapped out
    • madvise,提供管道告訴系統page管理方式如
      • MADV_RANDOM,期待記憶體page讀取行為是隨機的。
  • laze loading:allocate記憶體先給位址。等到process要存取的時候OS就會發現存取到沒摸過的記憶體,於是就產生page fault,這時候才去處理page分配的問題。

  • 每次page fault 就像kernel發動白金之星,然後就有一個白痴驚訝地發現自己樓梯怎麼走都走不完,但是他完全無法感受/了解發生了什麼事。

Fun with flags memory mapping

  • long page_size = sysconf(_SC_PAGESIZE);
    • 取得系統的page size。

Fixed memory mappings

投降輸一半,不懂Fixed memory mappings是啥、以及什麼時候需要這樣的東西。

File-backed memory maps

透過mmap,可以將檔案內容mmap到記憶體中,如此一來可以加快存取速度。配合參數,多個process可以共用同一塊記憶體。不過衍生出來有幾個問題

  1. 如何寫回檔案
    • man msync
    • 其他方式:fsync/pwrite
  2. 如何調整檔案/記憶體長度
    • man ftruncate

Copy-on-write

有些情況是一個process要吃別的process已經map到記憶體的內容,而不要把自己改過的資料放回原本的記憶體。也就是說最終會有兩塊記憶體(兩份資料)。當然每次都複製有點多餘,因此系統使用了Copy-on-write機制。要怎麼做呢?就是在mmap使用MAP_PRIVATE參數即可。

mmap不是萬能丹,極端情況下連續page fault可能會比原本的開檔獨資料還慢。


延伸參考資料,我都沒看

Using as 手冊筆記

| Comments

先承認我自己很不滿意這篇,太亂了。只能當工具查keyword用。不過as 手冊的確就是指令和語法。原本是以英文字母順序說明,我只是把這些用自認的方式重新分類。很多地方也真的只有句意翻譯。就把他當作看手冊的導讀,有找的需要的再進去看手冊吧。

本篇只討論ELF部份,其他binary format跳過。

目錄

as參數

只提幾個我有興趣的部份

  • -Z:硬上,就算有錯誤照樣組譯沒有錯的部份。
  • --gstabs+:好東西,可以幫你加入debug資訊,然後直接用gdb除錯。
  • 如果檔案副檔名為.s,就是普通的組合語言原始檔。
  • 如果檔案副檔名為.S,就可以使用cpp(還記得c preporcessor吧?)來處理前置處理。

名詞解釋

  • symbol:由字母、數字、和_.$組成的字串。不得以數字開頭。
    • labelsymbol後面加:
  • .開頭的symbol是gas 的directive
  • expression:運算式,結果代表不是位址就是單純的數字
  • 原始碼不是以上的情況,由英文字母開頭組成的字串就是instruction
  • 原始碼最後一行一定要是\n。目前網友Carl有提供為什麼這樣規定的link

常數

  • 字元常數:
    • '字元
    • 顯示\: '\\
  • 字串:"字串"

Section

一個連續的記憶體空間。這段連續空間都是為了處理某些單一特定的任務如執行程式碼、存放global變數等。

題外話,.bss存在的目的是節省儲存空間,沒有初始的全域變數當然不需要在檔案中保留儲存空間。

undefined section

在組譯的時段只要位址無法決定的symbol,一律放到undefined section。然後祈禱linker幫你搞定。

relocation

前面的文有提到,linker功能之一就是把不同的object檔案成一個執行檔。要怎麼呢?

每個object 檔案的起始點都是address 0。由linker計算並設定每個object檔案最後在執行檔放置的address,避免這些object的內容互相覆蓋。

而linker要怎麼搬移和設定最後的位址呢?這是因為object檔案內已經有規範好的不同名稱的好幾個連續空間,也就是section。所以linker把這些object檔案中相同section名稱的連續空間搬到執行檔內相同名稱的空間,並且保證執行檔內這些section的空間也是連續的。而搬移的動作並設定section的runtime address就稱為relocation

Linker在relocation時需要考慮的問題,as也幫他處理了,這些問題是

  • 目前這個位址要對應到object檔案的哪個地方?
  • 這個位址會需要佔用多少byte的空間?不懂?int和char吃的空間總會不一樣吧。
  • 目前位址對應到的是哪個section? 這個位址和對應section的offset為何?
  • 目前的位址是絕對位址還是和program counter相對的位址?

另外要注意的是,大部分的位址可以表示成

1
 (section) + (offset into section)

Expression

expression的結果代表不是位址就是單純的數字。這些數字要嘛是絕對位址、要嘛就是某個section的offset。而expression之間可以有空白。

Empty expression

空白字元或是null,其值會被設為0

Integer expression

由一個以上的argument和operator組成的expression

Arguments

包含 symbols, numbers 或subexpressions,分別討論

  • symbol:結果將會是 {section setction的offset數值},數值會是32位元的二的補數(就是有正負值啦)
  • numbers:一般來說,是正整數。如果你要處理浮點數或是大數(超過32位元的數字)as會噴警告。你需要自己處理這種情況。
  • subexpressions:指的是
    • (expression)
    • prefix operator 伴隨一個 argument

Operators

用來協助運算section中的offset位址。

  • Infix Operators
    • 就一般的binary operator如+, -
  • Prefix Operators
    • -:負號
    • ~:補數,就是將argument的每個位元inverse

Infix也和C語言一樣,有優先順序、符號定義也大致相同,列出如下

  • 最優先
    • *, /, >>, <<
  • 第二順位
    • |, &, ^, !
  • 第三順位
    • +, -, ==, <>, !=, >, <, >=, <=
      • <>就是!=
  • 最低順位
    • &&, ||

directives

重頭戲。directive又稱pseudo-ops,一律以.開頭。照字面理解,這東西是用來協助使用開發,而不是真正的CPU instruction。這邊我只列出看得懂我感興趣的部份。有興趣請參考出處。另外和硬體相依的directive請參考這邊

變數相關

  • .ascii "字串":可以用多個字串,中間以,隔開。這些字串最終會被一起放在連續的記憶體中。
  • .asciz "字串":和樓上的差別是字串後面會自動填\0,和C語言的字串表示方式相同。
  • .balign[wl] abs-expr, abs-expr, abs-expr:和.align差別在b是byte,w是2-byte,l是4-byte。這代表什麼呢?代表要pad的數字(如果有指定的話)要注意fill byte數量。如.balignw 8, 0xbeef
  • .byte expressionsexpression數量可以從0個到多個,中間以,隔開。這些expression會依照順序排列。那麼要幹什麼用呢?你可以這樣玩。
1
.byte 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x00 /* "Hello" */

還有其他玩法,請參考這邊這邊

  • .int expressions
  • .long expressions
    • 上面兩個有同樣效果,expression為16-bit寬度。可以用,隔開。和.byte用法類似。長度以及order會和CPU架構相關。
  • .hword expressions
  • .short expressions
    • 上面兩個有同樣效果,expression為16-bit寬度。可以用,隔開。和.byte用法類似。
  • .double flonums:就浮點數,可以用,隔開。和.byte用法類似。表示方式要看target CPU架構。
  • .float flonums:就浮點數,可以用,隔開。和.byte用法類似。表示方式要看target CPU架構。
  • .lcomm symbol, length:為symbol保留length的空間,該symbol型態不會是global,並且會被放在.bss section。
  • .octa 大數字:為16-byte寬度。可以用,隔開。和.byte用法類似。
  • .quad 大數字:為8-byte寬度。可以用,隔開。和.byte用法類似。
  • .string "字串":將字串放到object file中,看不出來和.ascii差在那。
  • .string16 "字串":將字串放到object file中,字串中的單個字元將會展開成2個bytes。看不出來和.ascii差在那。
  • .string32 "字串":將字串放到object file中,字串中的單個字元將會展開成4個bytes。看不出來和.ascii差在那。
  • .string64 "字串":將字串放到object file中,字串中的單個字元將會展開成8個bytes。看不出來和.ascii差在那。
  • .set symbol, expression:將symbol的值設成expression的值。
  • .size symbol, expression:設定symbol空間為expression的值。

Symbol的描述

visibility:local, global or weak

  • .extern:單純是相容性使用,特地列出來只是因為手冊說as將所有undefined symbols視為extern
  • .global symbol
  • .globl symbol
    • 以上兩個同樣效果,就是讓linker看得到這個symbol,也就是說透過nm觀察binary也可以看得到這些symbol
  • .local symbol:讓linker到這個symbol。手冊上另外有提到.local不支援alignment的問題和解法我看不懂,有興趣自行去連結參考。
  • .weak symbol:組譯器找不到symbol會產生一個。

Symbol type

  • .type symbol, type:type 描述方式有五種。我只用我看順眼的那種說明。
    • "function":這個symbol是個function
    • "object":這個symbol用來存放資料
    • "tls_object":這個symbol用來存放thread local資料
    • "notype":沒有指定
    • "gnu_unique_object":保證該symbol是唯一的symbol
    • "gnu_indirect_function":看不懂

其他Symbol 相關

  • .desc symbol, abs-expression:提供描述symbol的特性,細節請參考前面的說明。
  • .equ symbol, expression:將symbol設成expression的值
  • .equiv symbol, expression:和上面類似,但是如果該symbol之前已經定義過,就會噴錯誤。

Section

  • .data:不解釋
  • .test:不解釋
  • .section name:讓as把以下的東西組成name的section。名字雖然可以亂取,但是也要看binary format有沒有支援。如a.out就沒有這東西。

ELF 下的Section directive

ELF的話,這個directive有加料。說明如下: []表示optional

  • .section name [, "flags"[, @type[,flag_specific_arguments]]]
    • flags:可由下面的flag合體組成
      • a:allocatable,就是要在記憶體內吃空間,但是loader不一定會載入東西到該section
      • e:非executable或是shared library的section
      • w:可寫入
      • x:可執行
      • M:可被merge
      • S:該section有 zero terminated 字串
      • G:屬於某個section group
      • T:給thread local存放東西用 (存放三小?)
      • ?:看不懂,跳過
    • type
      • @progbits:section有資料 (怎麼有種廢話的感覺?)
      • @nobits:沒有資料,如.bss這樣的section
      • @note:不是給程式執行的時候使用的section
      • @init_array:該section 有個pointer arrary 指到init 函數(補充說明1 補充說明2)
      • @fini_array:該section 有個pointer arrary 指到fini 函數
      • @preinit_array:該section 有個pointer arrary 指到pre-init 函數

由於@在某些平台如ARM上是註解的符號,這種情況需要用%替代。

GM有特別規範,必須隔離在雙引號外面。而同時要用這兩個flag要以MG順序擺放,範例如下:

  • .section name , "flags"MG,...

Section group目前先假裝沒看到,有機會又看到再回來討論。

條件以及控制相關

  • if 部份有點雜亂,懶得想範例測試,想像成C語言的#ifdef。剩下自己看手冊
  • .irp symbol,values...:和巨集概念很類似,把.irp ....endr之間的instruction用到symbol的部份全部換成value。範例如下。

.irp item, 2, 3, 4 mov %r\item, $\item .endr

會展開成

1
2
3
mov %r2, $2
mov %r3, $3
mov %r4, $4
  • .irpc symbol,values...:手冊上面的說明幾乎和irp相同,悲劇的是範例和.irp完全一致。唯一差別是.iprc中有提到character,只能猜測c是character。
  • .offset loc:將locale counter設定成loc。
  • .org new-lc, fill:同樣是更動locale counter,但是只能在同一個section中移動。另外一個要注意的是這個指令只能增加locale counter,硬要減少是不可能的。當locale couter移動後,中間的空白會填入fill的值。不加上, fill as會填0。
  • .rept 次數:重複.rept.endr指定的次數。
  • .skip size, fill:產生size長度,fill值的資料。
  • .fill repeat, size, value:產生value,佔用空間為size。是否要產生多個,否的話repeat0,是的話repeat填要產生的個數。sizevalue為optional,size預設為1value預設為0

    • .fill 2,,
    • .fill 2,,10
    • .fill 2,4,
  • .warning "string":印出警告訊息。

  • .err:噴錯誤,除非as有-Z指令,不然別想產生obj檔。
  • .error "錯誤訊息":印出錯誤訊息然後GG。不帶錯誤訊息as會印出檔案名稱和用了.error那行。

  • .fail expressionexpression值大於五百噴警告,小於五百噴錯誤。用在複雜的巢狀巨集或是條件式組合語言中。

  • .print "字串":組譯的時候stdout會印出字串。
  • .end:表示組合語言程式結束

巨集

跳過,自行看手冊

ELF相關

  • .symver symbol, symbol2@nodename:指定symbol的版本號碼,一般用在shared library中。詳細說明懶得看,那天GG再回來看。

ELF section stack

  • .subsection name:把目前的section push到section stack中,並且把目前的subsection置換成name
  • .popsection:從section stack中pop最上面的section去覆蓋目前的section
  • .pushsection name [, subsection] [, "flags"[, @type[,arguments]]]:把目前的section push到section stack中,並且把目前的section置換成name以及subsectiontypeargument.section的參數相同。

ELF visibility

  • .protected symbol:不但外部看不到該symbol,連內部要使用讀取該symbol的另外一個symbol也要在內部定義。直接舉個虛擬C語言。
ex.c
1
2
3
4
5
static int whatever = 1;
void func(void)
{
    int local = whatever;
}
  • .hidden symbol:想像C語言在function前面加上static,觀念類似,讓該symbol無法被其他component看見。手冊這樣的symbol通常被視為.protect symbol,目前懶得寫程式測試。單純猜測這兩個有不同,不然幹嘛要分成兩個指令。
  • .internal symbol:手冊上提到除了和.hidden有同樣效果外,不同的CPU會針對這個symbol做特別處理,到底是哪些特別處理,手冊沒說。

除錯相關

大部分跳過,太多背景需要補完。

  • .def
  • .endef
  • .dim:給compiler產生除錯用。
  • .file 檔案行號 檔名DWARF2用的除錯,除錯時對應的原始碼行號。
  • .func name[,label]:只有開啟除錯有效,必須在結尾加入.endfunclabel就是組合語言內的label,也就是該function的進入點。不填的話,就在name加上prefix 字元當作進入點,通常prefix字元為_

  • .loc fileno lineno [column] [options]DWARF2用的除錯。整理如下

    • 手冊假設我們很瞭debug內部資訊,但是我不會。看下來他們有提到
      • .debug_line 狀態機
      • .debug_line line number matrix
      • 不明暫存器:is_stmt register,isa register等
    • 資訊放在binary 的.debug_line section。
    • 在debuger(?)載入.debug_line資訊時,讀到該行,會把參數fileno, lineno,等參數一併載入。
    • options:
      • basic_block:設定 .debug_line狀態為basic_block
      • prologue_end:設定 .debug_line狀態為prologue_end
      • epilogue_begin:設定 .debug_line狀態為epilogue_begin
      • is_stmt value:設定 is_stmt register 在.debug_line狀態為value,合法數值只有01
      • isa value:設定 isa register 在.debug_line狀態為value,合法數值只有01
      • discriminator value:設定 discriminator register 在.debug_line狀態為value,合法數值只有01
  • .loc_mark_labels enable:是否enble,basic_block register細節完全看不懂。只知道和debug line number entry有關。
  • .stabs symbol, type, other, desc, value:用來提供資訊給symbolic debuger。詳細資訊請看手冊
  • .tag structname:compiler產生的輔助directive。用來從symbol table中找出structname的instance。
  • .val addr:看不懂。自己看手冊。看起來是紀錄addr的值,但是怎麼會和symbol table扯上關係??

未分類

  • .include "file":從目前的位置,把file全部原封不動地放到之後的位置。
  • .align abs-expr1, abs-expr2, abs-expr3local counter (請參考前面linker script文)結束要對齊的位址倍數。
    • abs-expr1:必填。要對齊的數字。根據CPU,數字代表可能是byte,有些代表的是bit。所以要對齊8有的CPU要填8,有些CPU要填3。心理的OS,欠揍。
    • abs-expr2:optional。如果需要填空,可以指定填入的數值。不填就使用預設值,0。
    • abs-expr3:optional。指定跳過數字最多可以幾個,超過就直接不對齊了。(手冊用skip而不用pad讓我在想這到底差別在那?)

如果後面兩個都不想填,可以直接下.align abs-expr1,,收工。

  • .comm symbol, length:我是這樣理解啦,就是很多C語言檔案要用同一個全域變數。先摸到的先贏。可以看我以前整理的說明。另外有兩點要注意

    • 如果有同樣的symbol,在不同檔案中,設定的長度又不同,gas會選最大的。
    • ELF有隱藏的第3參數,用來指定alignment。
  • .gnu_attribute tag, value:GNU屬性自己查

  • .ident 字串:不同binary format有不同處理,在ELF中會把字串放到.comment section中。要注意,file包括command line參數中-I指定的路徑。
  • .incbin "file"[,skip[,count]]:從目前的位置,把file原封不動地放到之後的位置。你可以透過skip指定從檔案起始地幾個byte後跳過。另外你也可以透過count指定檔案最多include幾個bytes。

另外一點要注意,file包括command line參數中-I指定的路徑。

  • .version "string":產生.note section並且將字串放入該section。

參考資料

C 語言的潛規則型態轉換

| Comments

同樣是看過C Programming: A Modern Approach的筆記整理。一樣,寫的時候手上沒書,請自行斟酌,盡信書不如無書,更何況是組裝工亂寫的東西呢。

一般來說,變數型態可以強制轉換,這學過C 語言應該都知道。不過看了書上才發現不只強制轉換,compiler也會幫你的程式碼加料、做型態轉換。更恐怖的是,沒注意到的話,會發生慘劇。

本文開始之前,先定義專有名詞:

  • promotion
    • 當然不是升官還是促銷。而是把變數的型態升級,如short int -> int。為什麼要這樣幹呢?因為expression裏面變數不只一個,做運算的時候需要把某些變數升級,以免計算結果超過預期。諷刺的是,因為這樣的方式,也可能讓你的程式行為和你預期的不同。

書中提到潛規則型態轉換發生的時機如下

  • expression中變數型態不同
  • assign的left value和right value型態不同
  • 函數中的參數變數型態和函數內部使用該變數時的型態不同
  • 函數的回傳值和宣告的不同

後面兩種大概就是下面這樣的情況吧

1
2
3
4
5
6
long test(int i)
{
    i = 0.5f;

    return i;
}

書中用下面的方式分類C 語言的潛規則型態轉換規則,分別整理如下,偷懶不講C99了。

assignment

這個最簡單,等號左邊用啥型態右邊就得轉成這樣的型態。這也是為何

1
2
int i = 11.039;
printf("%d", i);

結果會是11的原因。

expression

一樣這是compiler的術語。此這邊就是一堆的歡樂的型態排列組合,C99還有_Complex_Bool參戰。

一樣先分類

  • expression中的變數只有小數點那種型態,依下面順序promote: float -> double -> long double
  • expression中的變數只有整數,就變成
    • promote時要判斷使用unsigned 還是signed整數依照下面的順序promote:
      • int -> unsigned int -> long int -> unsigned long int
    • 這種情況沒注意的話,可能就會出現下面的悲劇
implicit.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
void implicit_conv()
{
    int i = -1;
    unsigned int j = 100;

    if (i < j) {
        printf("expected\n");
    }
    else {
        printf("GG\n");
    }
}

int main(int argc, char **argv)
{
    implicit_conv();

    return 0;
}

執行結果如下

1
2
3
4
5
$ make implicit
cc     implicit.c   -o implicit

$ ./implicit
GG

為何會GG呢,因為 i < j是一個expression,照上面的規矩,expression同時有intunsigned int的話,int會被promote的unsigned int-1的二進位不知道的人,可能要先搞懂再來學C吧?

如果你擔心程式有類似的問題,可以把gcc最囉唆的檢查打開,就會噴出錯誤如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ make implicit CFLAGS="-Wall -Wextra -Werror"
cc -Wall -Wextra -Werror   -c -o implicit.o implicit.c
implicit.c: In function ‘implicit_conv’:
implicit.c:7:11: error: comparison between signed and unsigned integer expressions [-Werror=sign-compare]
     if (i < j) {
           ^
implicit.c: In function ‘main’:
implicit.c:15:14: error: unused parameter ‘argc’ [-Werror=unused-parameter]
 int main(int argc, char **argv)
              ^
implicit.c:15:27: error: unused parameter ‘argv’ [-Werror=unused-parameter]
 int main(int argc, char **argv)
                           ^
cc1: all warnings being treated as errors
make: *** [implicit.o] Error 1

你可能會問,那有小數點的型態和整數型態亂戰會怎樣呢?自己估狗或看書吧。

題外話

題外話一

由於這些潛規則,在轉換型態的時候,可以看到這樣的statement。

1
2
double whatsoever;
whatsoever = (double) 10 / 3;

為什麼3不用型態轉換呢?你必須要比對C語言的Operator precedence,也就是運算元處理順序。可以知道C 語法會處理順序如下

  • 10 cast成double型態
  • 因為潛規則,3也會被promote成double
  • 計算10 / 3
  • 將結果assign給 = 左邊的變數

題外話二

在C語言中有小數點的常數預設型態可是double唷。所以你如果有float最好使用下面的方式轉換型態。至於為什麼會規定是double呢?書中有講八卦,就不破梗了。

1
float whatsoever = 3.14f;

致謝

  • 2016/Oct/28: 感謝網友Jeffery Chang糾正typo

C 語言的format String

| Comments

format string也是看過C Programming: A Modern Approach才注意的。寫文的當下沒帶這本書,問男人好了。

1
$ man 3 printf

簡單來說,format string是由

  • 普通文字,不包含單獨的%字元
  • conversion specifications,就是你看到的%s之類的的東西
  • 每個conversion specifier有對應的參數

整理conversion specifier如下

示意如下

  • %[Flag][最少欄位寬度規範][精確度描述][變數size描述]Conversion specifier

接下來依上面的部份說明如下

Flags

  • #
    • 自動對指定的數字進位數(八進位、十進位、十六進位等)加上合適的prefix如0x, 0等。
  • 0
    • 填入0作為pad。pad是啥呢?中文意思是填充物,自己體會吧。
  • -
    • 預設format string是靠右對齊,用了這個會變成靠左對齊。和樓上0一起會不會數字多好幾倍如4324320000?不會,兩個同時出現0會被省略。
  • +
    • 指定數字最前面要加正負號
  • ' '
    • 如果有設定數字要先顯示正負號,會在前面多加一個空白

以上是C 語言規範的,不同編譯器有加碼,懶得寫,自己問男人吧。

最少欄位寬度規範

一定要正整數,當印出的數字長度少於這邊指定的參數,會自動填空白或是0。印出超過這邊指定的寬度的話呢?超過就超過,不然要怎麼辦?

另外預設是往右邊對齊,想要往左對齊,請參考- flag。

精確度描述

.十進位數字表示,不同的變數型態有不同的精確度定義。

舉例來說

  • 整數,最少要出現的數字長度,少於這樣的數字,會直接在左邊或是基底符號(0, 0x)後面填零補完。
  • 實數,也就是有小數點的,就是小數點後面最多可以出現的數字長度,超過了就截掉。

變數size輔助描述

一般來說,我們會知道d是整數,但是在C語言還是有long intlong long int這樣型態的整數,為了能夠更精確的顯示,format string提供了這樣的描述,讓你加在Conversion specifier,如

  • l: long
  • h: short

詳細列表和排列組合請問男人。

其他

flag除了C規格定的以外,不同廠商有加料,一樣去問男人,不過要用這個的話要考慮porting的問題。

除此之外,男人有列出所有的Conversion specifier,除了熟悉的d, f, s, c, x以外,我列幾個我感興趣的如下

  • a, A: C99專用,實數的十六進位表示法
  • p: void * 的位址

怪招

format string還有*%m$這種鬼東西,目前搞不清楚為什麼要這樣幹。 不過幾然花了時間搞懂,就整理一下

  • *指目前對應的參數的下一個參數
  • %m$*n$把第n個的參數和第m個顯示交換

不知道是什麼鬼對不對?我也是,所以寫了程式測試一下

test_format_string.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main(int argc, char **argv)
{
    int i = 2;
    int j = 3;
    int k = 4;

    printf("@@%*d\n", i, j);

    /* print third argument then first argument */
    printf("!!%3$*1$d, %1$*2$d\n", i, j, k);

    return 0;
}

看看輸出吧,一樣懶得寫Makefile

1
2
3
4
5
6
$ make test_format_string
cc     test_format_string.c   -o test_format_string

$ ./test_format_string
@@ 3
!! 4,   2

結論

這篇文章文章我介紹了

  • format string和他的語法簡單說明
  • make 的implicit rule
  • 取得Linux 下執行程式結束回傳值

參考資料

C 語言的逗號

| Comments

因為只能和 C語言裝熟,只好看書看能不能裝更熟點。感謝成大同學推荐的書本:C Programming: A Modern Approach。今天看到逗號的用法,手癢來用一下。

簡單來說,,就是把expression串在一起,然後回傳最後一個expression的值。注意的是expression是compiler專有名詞,自己看連結,不解釋。

知道這個特性,當然來個小實驗。照慣例還是要講一下測試環境。

1
2
3
4
5
6
$ lsb_release -a
No LSB modules are available.
Distributor ID:   Ubuntu
Description:  Ubuntu 14.04.2 LTS
Release:  14.04
Codename: trusty

接下來寫個小程式:

comma.c
1
2
3
4
5
6
7
8
#include <stdio.h>
int main(int argc, char **argv)
{
    int i = 0;
    int j = 10;

    return -1, i + j;
}

程式太簡單,懶得寫Makefile,直接make:

comma.c
1
2
$ make comma
cc     comma.c   -o comma

接下來就是驗收

comma.c
1
2
3
$ ./comma
$ echo $?
10

接下來要問問題時間

  • 為啥要這樣幹?
  • 這個要用在這什麼地方?

先回答第一個問題,就是要把一堆expression擠到被認為是一個expression。有了這樣的想法後,很多語法會只有一個express,如if, for, while,這時候想要在裏面多做事想得不得了的時候就可以用,,不過和條件有關的情況下,記得注意僅回傳最後一個expression的特色。

題外話,這個,目前我印象中除了變數宣告和for以外,只看過一次,那次看到讓我有點反應不過來,感覺單純只是想省掉{}而已。

Hello Linux ARM 組合語言

| Comments

目錄

前言

之前的文章有不少在討論執行檔該長怎麼樣。簡單來說,一個執行檔會有

  • Sections:程式行為和資料會分開放在不同的sections
  • 進入點,也就是system call開始執行你的程式的地方

以這樣的觀點,來看組合語言,會比較有感覺。

這次主要想要試看看如何使用組合語言印出Hello world。學過作業系統的朋友應該知道OS真正提供給使用者的介面叫作system call。有興趣的朋友可以使用strace研究執行檔呼叫了那些system call。這次的Hello world我有兩個線索

  • 在command line執行的process會有3個馬上可以使用的file descriptor(不知道那啥的請自行估狗)分別是
    • 0: standard in
    • 1: standard out
    • 2: standard error
  • 有一個system call叫作write,你可以透過他把任何資料寫到指令的file descriptor

綜合以上,我們要幹的事就是透過組合語言做出

1
write(1, "Hello world\n", 12);

這又表示組合語言中我們要做

  • 呼叫system call
  • 帶參數給system call,這部份需要有
    • ABI的背景知識
    • 定址方式,更精確的說,如何宣告"Hello world\n",讓runtime時放在在process address space中,並將它的位址傳給system call

測試環境

  • Host
1
2
3
4
5
6
$ lsb_release -a
No LSB modules are available.
Distributor ID:   Ubuntu
Description:  Ubuntu 14.04.2 LTS
Release:  14.04
Codename: trusty
  • Guest OS on Qemu
    • 這邊很奇怪我的kernel用更新過的版本Qemu完全無法開機。目前裝死中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ lsb_release -a
No LSB modules are available.
Distributor ID:   Debian
Description:  Debian GNU/Linux 8.0 (jessie)
Release:  8.0
Codename: jessie

$ cat /proc/cpuinfo
Processor : ARM926EJ-S rev 5 (v5l)
BogoMIPS  : 643.48
Features  : swp half thumb fastmult vfp edsp java
CPU implementer   : 0x41
CPU architecture: 5TEJ
CPU variant   : 0x0
CPU part  : 0x926
CPU revision  : 5

Hardware  : ARM-Versatile AB
Revision  : 0000
Serial        : 0000000000000000

範例:版本一

我本來想說慢慢來,先來個完全沒意義的r0 = 0; r1 = 1; r2 = r0 + r1。程式如下:

hello.s
1
2
3
4
5
6
7
.text
.global _start
_start:
    mov %r0, $0
    mov %r1, $1
    add %r2, %r0, %r1
.end

幾點說明:

  • $或是#代表一個數字(出處)
  • %r1代表ARM的r1暫存器,但是為何用%目前沒找到手冊上有說明。
  • .text前面的文章有看應該覺得很眼熟,就是告訴編譯器以下是程式行為。
  • global是讓symbol可以外露,白話來說就是nm等binutil可以看的到這個symbol。
  • _start是一個程式執行的起始點,有看過之前文章就會覺得很眼熟。
  • .end表示程式結束點,不過目前用起來有沒有加好像沒有差別。

  • Makefile

Makefile
1
2
3
4
5
6
7
8
TARGET=hello
AS_FILE=$(addsuffix .s, $(TARGET))

$(TARGET): $(AS_FILE)
  $(AS) $^ -o $@

clean:
  rm -rf $(TARGET)

想法很簡單,就是直接編譯應該可以跑,雖然完全不會有畫面。錯!跑出來會這樣

1
2
3
4
5
6
7
8
$ make
as hello.s -o hello

$ ls -gG
total 12
-rw-r--r-- 1 588 Apr 20 18:04 hello
-rw-r--r-- 1  83 Apr 20 17:58 hello.s
-rw-r--r-- 1 113 Apr 20 17:58 Makefile

這代表什麼,hello編譯完後的binary本身沒有執行權限。改改權限看可不可以跑?

1
2
3
$ chmod +x hello
$ ./hello
bash: ./hello: cannot execute binary file: Exec format error

怎麼回事?分析一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ readelf hello -h
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           ARM
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          268 (bytes into file)
  Flags:                             0x5000000, Version5 EABI
  Size of this header:               52 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           40 (bytes)
  Number of section headers:         8
  Section header string table index: 5

看不出來對不對?我是這樣啦,所以先比對/bin/ls的輸出吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ readelf /bin/ls -h
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           ARM
  Version:                           0x1
  Entry point address:               0x14354
  Start of program headers:          52 (bytes into file)
  Start of section headers:          99372 (bytes into file)
  Flags:                             0x5000202, has entry point, Version5 EABI, soft-float ABI
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         9
  Size of section headers:           40 (bytes)
  Number of section headers:         27
  Section header string table index: 26

仔細看一下Type:/bin/lsEXEChelloRELman elf可以看到REL是relocate file,那是啥呢?根據System V Application Binary Interface - DRAFT - 10 June 2013第四章的簡介,簡單來說就是object檔案,也就是說link時吃的檔案。所以我們加入Link吧。

範例:版本二

單純加入linker看看會怎樣?

Makefile
1
2
3
4
5
6
7
8
9
TARGET=hello
AS_FILE=$(addsuffix .s, $(TARGET))

$(TARGET): $(AS_FILE)
  $(AS) $^ -o $@.o
  $(LD) $@.o -o $@

clean:
  rm -rf $(TARGET)

kerker,一樣GG。

1
2
3
4
5
$ make
as hello.s -o hello.o
ld hello.o -o hello
$ ./hello
Illegal instruction

估狗到的組合語言的Hello world範例最後會呼叫exit system call,照著呼叫exit就可以正常結束,這就是第三版。至於為何會出現Illegal instruction,根據Scott Tsai大大的提示,當你的程式碼跑完後,接下來記憶體有啥CPU就跑啥,跑到不認識的opcode當然就GG了。

範例:版本三

hello.s
1
2
3
4
5
6
7
.text
.global _start
_start:
    mov     %r0, $0
    mov     %r7, $1
    svc     $0
.end

單純叫了exit而已,有幾點注意的 根據Debian ARM system call interface,可以知道

  • r0 ~ r6是函數的帶入參數
  • r7 是system call number,而system call number是啥呢?就是你要的system call 對應的數字。

所以要呼叫exit(0) system 表示

  • 傳一個參數,數值為0
  • 要設定exit對應的system call number為1

為何system call number是1呢?可以看看unistd.h裏面system call number的定義,exit的system call number為1

設定完傳給exit參數後呼叫了一個組合語言指令svc,這個指令主要是切換到Supervisor模式。Linux下面也許太過複雜,不太容易從user mode一路追到kernel然後又看懂這些system call的行為。沒關係成大資工作業有比較看得懂的範例可以參考。例如包裝呼叫system call的函數以及Kernel中對應的system call服務實作

根據unistd.h定義的system call number,write的system call number 是4,所以我們可以開始寫最後的版本了。

範例:版本四

開始之前,先來看男人怎麼介紹write system call的

1
ssize_t write(int fd, const void *buf, size_t count);

這代表

  • 有三個參數要傳給system call
  • 有回傳值可以吃
  • 其中一個參數是位址,這個位址我們會放"Hello World\n"字串

那麼我先來看看怎麼放字串到記憶體

1
2
3
.data
hello_str:
    .ascii "Hello World\n"

.data如果有看我以前的文章,就知道這是放有初始值全域變數的地方。

hello_str呢?嗯,對_start:有印象嗎?

這叫作label,是GNU組語中symbol的一種,有興趣可以看這邊。根據手冊,label還有一個功能,代表目前跑到的位址。所以_start:就是.text section的起始位址。而hello_str就是.data section的起始位址。從這兩個label可以看到label只是一個位址,可以指向函數或是資料,這和C語言的指標有異曲同工之妙。有興趣的朋友可以去找function pointer和C語言的callback函數。

.ascii,單純就是宣告字串指令。

呼叫write system call來還有兩個問題要處理

  • 如何取得hello_str對應的位址放到暫存器r1上面?
  • 要怎麼算出hello_str字串的長度?

關於第一個問題,GNU ARM組合語言有中將數值或位址放到暫存器的虛擬指令

1
ldr <register> , = <expression>

expression是一種表示位置或是數值的方式。

恰巧symbol也算是一個expression,所以可以表示成:

1
ldr %r1, =hello_str

第二個問題呢?要介紹.了。之前看過linker script的朋友應該對於locale counter還有印象。locale counter代表目前的位置。加上expression也支援運算。利用hello_str.data 開頭,我們可以這樣做:

1
2
3
4
.data
hello_str:
    ascii "Hello World\n"
hello_len = . - hello_str

綜合上面的討論,版本四組合語言會是

hello.s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.data
hello_str:
    .ascii "Hello World\n"
hello_len = . - hello_str

.text
.global _start
_start:
    /* %r0 = write(1, hello_str, hello_len); */
    mov     %r0, $1
    ldr     %r1, =hello_str
    ldr     %r2, =hello_len
    mov     %r7, $4
    svc     $0

    /* exit(%r0) */
    mov     %r7, $1
    svc     $0

.end

等等,不是說有回傳值?還是要看Procedure Call Standard for the ARM Architecture ABI r2.09手冊,上面提到回傳資料會放到r0,剛好接下來的exit system call帶的第一個參數也要存放在r0,那麼我們可以直接觀察執行後回傳值如下:

1
2
3
4
5
6
7
8
$ make
as hello.s -o hello.o
ld hello.o -o hello

$ ./hello
Hello World
$ echo $?
12

補充

1
ldr <register> , = <expression>

這是個有趣的指令,這個指令事實上是個虛擬指令。怎麼說呢,因為這個指令的目標是把數值塞到指定的暫存器。這個數值是位址還是啥死人骨頭並不重要。重要的是,由於opcode的限制,把數值塞到指定的暫存器會有限制滴。例如ARM Cortex M0MOV的數值只有8-bit,要塞32-bit的數值就需要配合其他的指令做連續技。因此

1
ldr <register> , = <expression>

這樣的指令就可以讓你寫起來比較輕鬆。

另外一個值得一提是,如果組譯器無法把ldr <register> , = <expression>虛擬指令轉換成MOVMVN指令,把你的數值塞到暫存器的話。組譯器會把你的值放在一塊記憶體中,使用真的ldr把這塊記憶體的值載入到暫存器中。這個方法稱為literal pool,細節可以看這邊

總結

本文從會GG的組合語言一路改到可以印出Hello World,並且在程式結束後回傳字串長度。在文章中簡單提到了GNU as的

  • 組合語言中section
  • 組合語言的編譯方式
  • 組合語言中的symbol和字串表示方式
  • 組合語言中的expression
  • ABI實例

希望對有需要的朋友有所幫助。

延伸閱讀及致謝

感謝Scott Tsai大大提供的資料以及指出文章中錯誤的地方。另外他也有提到其他有趣的部份,當作以後的作業。先列出如下

  • 從組合語言直接呼叫header file內的system call
  • Kernel Memory Layout on ARM Linux
    • 這邊主要討論的是objdump -d發現obj檔和執行檔差別只在進入點位址的差別,而進入點位址如何決定呢?這邊有規範,另外也可以看default linker script看看如何設定的。
  • Scott大大的範例程式
    • 可以看到,這個版本和我參考寫出來的版本差異有
      • "Hello world"放在.rodata section中,這比.data更實際,因為這個字串的確沒有必要設成全域變數。
      • 使用了preprocess方式。
      • 提供了反組譯的結果
      • 提供X86-64版本的組合語言比較

感謝Viler Hsiao大大寫文回答我的問題。

參考資料