1. 程式人生 > >AT&T x86_32 匯編_002_使用C庫函數

AT&T x86_32 匯編_002_使用C庫函數

響應 很難 inux libc 中斷向量 vendor ESS 一個 mov

上一講介紹了一個簡單的示例程序, 並且我們提到了匯編寫代碼的一個優點: 你可以在硬件這上的, 任何軟件抽象層次去實現功能. 上一節我們輸出CPU廠商信息, 使用sys_write系統調用輸出內容, 這一節, 我們簡單的介紹一一上, 在上一講的基礎上, 如何調用libc中的printf函數來輸出內容.

1. 系統調用

我這樣的鹹魚程序員(非科班出身, 基礎不紮實, 自學成材的半路出家的鹹魚), 由於缺乏對計算機科學很多基礎知識的學習與認知, 所以有很長一段時間, 我很難深刻理解庫, 函數, 系統調用, 軟硬中斷等基礎概念. 甚至在自覺成材的過程中, 編譯鏈接這兩個基礎過程, 都困擾了我很長時間. 我相信有不少同學至今還和我一樣, 對一些基礎知識的掌握很不到位.

系統調用其實也是函數, 可以簡單的理解為C函數, 但是這種函數的調用有點特殊:

  1. 你不能通過簡單的引用頭文件, 鏈接庫的方式調用這些特殊的函數, 而是需要像上一節示例程序展示的那樣, 調用一個特殊的指令, 進行一次軟中斷, 在Linux中, 這個中斷號即是$0x80
  2. 這些函數, 也就是系統調用, 涉及的功能方面一般有: 設備管理, 文件管理, 進程控制, 進程通信, 內存管理等. 可以看出, 對於一個運行中的"系統"來說, 這些操作都是高度敏感的. 所以操作系統將這些操作的具體實現, 封裝成了系統調用. 而使用系統的用戶(在這個場景下, 所謂的用戶其實就是在操作系統上編程的程序員), 要進行設備操作, 文件操作, 進程操作, 內存操作等, 就不能直接接觸硬件驅動程序, 而必須通過操作系統提供的系統調用去實現.
  3. 或許機智的你想, 系統調用的實現, 具體到cpu上也是指令而已, 那麽我能不能寫一段相同的指令, 直接去指揮硬件, 操作文件, 操作內存呢? 答案肯定是否定的, 你可以寫出相同的指令, 進行編譯鏈接, 搞成可執行文件. 但在具體運行時, cpu會拒絕運行這些指令: 因為你權限不夠!! 即, 系統調用是運行在所謂的"核心態"的, 而一般的, 不涉及敏感操作的函數, 是運行在"用戶態"的. 核心態中的很多操作, 即CPU指令, 運行時需要切換cpu權限狀態. 這就限死了, 普通用戶要執行敏感操作, 只能通過系統調用去實現
  4. 需要謹記的是, 在你操作系統上運行的程序, 是要受操作系統代碼監管的. 操作系統的監管, 最簡單直接的方面, 分為兩部分:
    1. 操作系統在正常情況下, 將cpu設置為權限較低的權限狀態. 這保證了你的程序如果試圖執行一些敏感操作, cpu將直接拒絕, 你只能向操作系統寫申請, 由操作系統去執行這些敏感操作. 再將執行結果反饋給你. 顯然, 也只能操作系統的代碼能更改cpu的權限狀態.
    2. 通過執行敏感操作, 比如內存分配, 如果你向操作系統提交的申請很過分, 操作系統會拒絕執行.
  5. cpu提供的int指令, 即是軟中斷指令, 其實是一個信號傳遞機制. 或者說簡單一點, 是一個回調函數機制: 操作系統內核代碼事先寫一張回調函數表(軟中斷向量表). 這張表其實就類似於告訴cpu:"聽著, 當四號中斷發生的時候, 你就跳到這一段寫顯存的指令中去執行, 執行完了再回到原地". 用戶態的普通程序只能用int指令去觸發中斷, 類似於激發事件. cpu響應軟中斷, 查表執行對應的內核函數. 在執行時, 顯然這些內核函數的第一件事就是: 切換cpu權限狀態. 函數執行結束前夕, 會重新將cpu權限狀態恢復為較低的狀態.
  6. 系統調用的執行過程, 和普通函數調用, 最大的不同是:
    1. 普通函數的執行, 是在進程空間就地壓函數棧(用戶態進程棧), 執行結束後退棧. 調用前將現場信息(寄存器值等)壓在棧裏, 調用結束後再從棧中讀取這些信息, 重新恢復現場.
    2. 而系統調用的執行, 也有一個壓棧, 調用結束退棧恢復現場的過程. 但是! 這個棧, 每個進程一般只有4k, 剛好是一頁大小. 這就是所謂的"內核棧". 這和用戶態函數調用的"進程棧", 不是一個東西.

上面列出的幾點中, 有錯誤, 但不影響你(一個普通程序員)對程序執行流程的理解. 有興趣深挖的話, 去讀Linux內核相關的書, 探究一下.(我天分不夠, 讀不懂)

所以, 我們執行系統調用時, 是在匯編代碼裏發出一個int $0x80信號, 而操作系統內核代碼受軟中斷向量表回調喚醒後, 如何在內核棧中保存現場, 系統調用執行結束後如何回傳執行結果, 以及如何退內核棧, 返回用戶態. 這些我們都可以不用關心.

但是! 如果我們要調用的是一個普通的函數, 調用過程中如何壓棧, 保存現場, 調用結束後如果獲取執行結果, 退棧恢復現場, 就得我們自己動手了.

那麽, 如果我們調用的是printf函數呢? 這裏不要亂, 不要覺得, 啊, printf函數內部也肯定調用的是sys_write系統調用, 所以怎麽怎麽怎麽樣. 腦子不要亂. 無論printf內部是怎麽折騰的, 都和我們調用者無關, 對於我們來說, 這就是一個C函數, 所以我們要做的事情很簡單:

  1. 傳遞參數, 壓棧, 保存現場, 然後跳轉至printf中去.
  2. 當執行流程從printf返回回來後, 退棧, 恢復cpu現場即可

至於printf內部, 是如何調用sys_write系統調用的, 和我們卵關系都沒有

2. 在匯編中調用printf函數

下面是示例代碼, 功能和上一講的cpuid程序完全一樣, 不同的是, 這次調用的是printf函數來輸出內容, 而不是sys_write系統調用:

.section .data
output:
    .asciz "The processor Vendor ID is ‘%s‘\n"

.section .bss
    .lcomm buffer 12

.section .text
.globl _start
_start:
    # 調用cpuid獲取廠商信息
    movl $0, %eax
    cpuid

    # 把廠商信息的12個字符, 放在buffer中
    movl $buffer, %edi      # 把buffer的首地址放在edi寄存器中
    movl %ebx, (%edi)       # 把ebx中的內容放入edi寄存器所指向的內存中去, 其實就是buffer
    movl %edx, 4(%edi)      # 同上, 只不過向後偏移了四字節
    movl %ecx, 8(%edi)      # 同上, 只不過向後偏移了八字節

    # 壓棧, 其實就是壓 printf(output, buffer) 調用中的兩個參數
    pushl $buffer
    pushl $output

    # 調用printf
    call printf

    # 退棧, 讓指令指針+8即是退棧.
    addl $8, %esp

    # 壓棧, 其實就是壓 exit(0) 調用中的唯一一個參數
    pushl $0
    call exit

從上面的代碼中看, 保存現場與恢復現場中, 有很多細節隱藏在call這條語句中了, 後續會對它進行細節解釋, 目前不必要過分糾結細節, 主要是理解了函數調用的過程就好了.

這個代碼的編譯後的鏈接需要註意一下, 首先, 它使用了libc中的函數, 因此鏈接時需要加上-lc選項. 其次, libc的默認鏈接方式是動態鏈接, 除了在鏈接時要指明"動態鏈接哪個庫(也就是libc)"外, 還需要指定"由誰來負責動態鏈接". 所以需要加上-dynamic-linker /lib/ld-linux.so.2參數. 在Linux操作系統下, 動態鏈接程序都是由這個動態鏈接器負責執行動態鏈接的.

所以, 最終的鏈接命令可能有一點復雜, 但總之就是多了兩個地方:

  1. 要額外聲明, 程序的運行需要動態庫libc. 鏈接時對於libc庫是動態鏈接, 故加-lc即可
  2. 要指明動態鏈接器, 即-dynamic-linker /lib/ld-linux.so.2

編譯, 鏈接, 運行如下gif圖所示:

技術分享圖片

AT&T x86_32 匯編_002_使用C庫函數