Linux 的 x86 彙編程式設計(轉)

BSDLite發表於2007-08-11
Linux 的 x86 彙編程式設計(轉)[@more@]本質上來說, 這篇文章是把我最感興趣的兩樣程式設計東西: Linux 作業系統和組合語言程式設計結合在一起. 這兩個都不(或者說應該不)需要介紹; 像 Win32 的彙編,Linux 的彙編執行在 32 位的保護模式下...但它又有一個截然不同的優勢就是它允許你呼叫 C 的標準庫函式和 Linux 的共享庫函式. 我開始給 Linux 下的組合語言程式設計來個簡要介紹; 為了更好讀一點, 你可能要跳過這個基本的小節.

  編譯和連結
  ---------------------
  Linux 下兩個最主要的彙編器是 Nasm(free, Netwide Assembler)和 GAS(free, Gnu Assembler),
  後一個和 GCC 結合在一起. 在這篇文章裡我將集中在 Nasm 上, 把 GAS 放在後面,因為它使用 AT&T 的語法, 需要一個長的介紹.
  Nasm 呼叫時應該帶上 ELF 格式選項("nasm -f elf hello.asm"); 產生的目標檔案用GCC 來連結("gcc hello.o"), 產生最終的 ELF 二進位制程式碼. 下面的這個指令碼可用來編譯 ASM 的模組; 我儘量把它寫得簡單, 所以所有它做的就是接受傳給它的第一個檔名, 用 Nasm 編譯, 用 GCC 來連結.
  #!/bin/sh
  # assemble.sh =========================================================
  outfile=${1%%.*}
  tempfile=asmtemp.o
  nasm -o $tempfile -f elf $1
  gcc $tempfile -o $outfile
  rm $tempfile -f
  #EOF =================================================================

  基本知識:
  ----------
  當然最好的就是在瞭解系統細節之前從一個例子開始. 這裡是一個最基本的"hello-word" 形式的程式:
  ; asmhello.asm ========================================================
  global main
  extern printf
  section .data
  msg db "Helloooooo, nurse!",0Dh,0Ah,0
  section .text
  main:
  push dword msg
  call printf
  pop eax
  ret
  ; EOF =================================================================
  綱要: "global main" 必須宣告為全域性的(global) -- 並且既然我們用 GCC 來連結,進入點必須以 "main" 來命名 -- 從而裝入系統. "extern printf" 只是一個宣告,為以後在程式中呼叫; 注意這是必須的; 引數的大小不需要宣告. 我已經把這個例子用標準的 .data, .text 分節, 但這不是嚴格必須的 -- 可能只需要一個 .text段, 就像在 DOS 下一樣.
  在程式碼的主體部分, 你必須把引數壓棧來傳遞給呼叫. 在 Nasm 裡, 你必須宣告所有不明確資料的大小; 因此就有 "dword" 這個限定詞. 注意和其他彙編器一樣,Nasm 假設所有的記憶體/標號的引用都指的是記憶體地址或者標號, 而不是它的內容.
  因而, 指明字串 msg 的地址, 你應該使用 push dword msg, 指明字串 msg 的內容, 應該用 push dword [msg] (這隻能包含 msg 的前四個位元組). 因為 printf
  需要一個指向字串的指標, 我們應該指明 msg 的地址.
  呼叫 printf 非常的直接. 注意每一次呼叫後你必須把棧清除(見下); 所以 PUSH 了一個
  dword 後, 我從棧裡把一個 dword POP 進一個無用的暫存器. Linux 程式只簡單的用一個 RET 來返回系統, 由於每個程式都是 shell(或者是 PID)的產物, 所以程式結束後把 控制權還給它.
  注意到在 Linux 下, 你是在 "API" 或中斷服務的場所裡使用系統帶來的標準共享庫.

  所有的外部引用由 GCC 管理, 它給 asm 程式設計師節省了大部分的工作. 一旦你習慣了基本的技巧, Linux 下的彙編程式設計實際上要比 DOS 簡單的多.

  C 呼叫的語法
  --------------------
  Linux 使用 C 的呼叫模式 -- 意味著引數以相反的順序進棧(最後一個最先), 呼叫者必須清
  除棧. 你可以從棧裡把值 pop 出來:
  push dword szText
  call puts
  pop ecx
  或者直接修改 ESP:
  push dword szText
  call puts
  add esp, 4
  呼叫的返回值在 eax 或 edx:eax 如果值大於 32 位的話. EBP, ESI, EDI, EBX 由呼叫者
  儲存和恢復. 你必須儲存你要使用的暫存器, 像下面這樣:
  ; loop.asm =================================================================

  global main
  extern printf
  section .text
  msg db "HoodooVoodoo WeedooVoodoo",0Dh,0Ah,0
  main:
  mov ecx, 0Ah
  push dword msg
  looper:
  call printf
  loop looper
  pop eax
  ret
  ; EOF ================================================================

  粗一看, 非常簡單: 因為你在 10 個 printf() 呼叫用的是同一個字串, 你不需要清除棧. 但當你編譯以後, 迴圈不會停止. 為什麼? 因為 printf() 裡什麼地方用了 ECX 但沒有儲存. 使你的迴圈正確的工作, 你必須在呼叫之前儲存 ECX 的值, 呼叫之後恢復它, 像這樣:
  ; loop.asm ================================================================
  global main
  extern printf
  section .text
  msg db "HoodooVoodoo WeedooVoodoo",0Dh,0Ah,0
  main:
  mov ecx, 0Ah
  looper:
  push ecx ;save Count
  push dword msg
  call printf
  pop eax ;cleanup stack
  pop ecx ;restore Count
  loop looper
  ret
  ; EOF ================================================================

I/O 埠程式設計
  --------------------
  但直接訪問硬體會怎麼樣呢? 在 Linux 下你需要一個核心模式的驅動程式來做這些工作... 這意味著你的程式必須分成兩個部分, 一個核心模式提供硬體直接操作的功能, 其他的使用者模式提供介面. 一個好訊息就是你仍然可以在使用者模式的程式中使用IN/OUT 來訪問埠.
  要訪問埠你的程式必須取得系統的同意; 要做這個, 你必須呼叫 ioperm(). 這個函式只能被有 root 許可權的使用者使用, 所以你必須用 setuid() 使程式到 root 或者直接執行在 root 下. ioperm() 的語法是這樣:
  ioperm( long StartingPort#, long #Ports, BOOL ToggleOn-Off)
  StartingPort# 指明要訪問的第一個埠值(0 是埠 0h, 40h 是埠 40h, 等等),#Ports
  指明要訪問多少個埠(也就是說, StartingPort# = 30h, #Port = 10, 可以訪問埠
  30h - 39h), ToggleOn-Off 如果是 TRUE(1) 就能夠訪問, 是 FALSE(0) 就不能訪問.
  一旦呼叫了 ioperm(), 要求的埠就和平常一樣訪問. 程式可以呼叫 ioperm() 任意多次,
  而不需要在後來呼叫 ioperm()(但下面的例子這樣做了), 因為系統會處理這些.
  ; io.asm ==============================================================
  =
  BITS 32
  GLOBAL szHello
  GLOBAL main
  EXTERN printf
  EXTERN ioperm
  SECTION .data
  szText1 db Enabling I/O Port Access,0Ah,0Dh,0
  szText2 db Disabling I/O Port Acess,0Ah,0Dh,0
  szDone db Done!,0Ah,0Dh,0
  szError db Error in ioperm() call!,0Ah,0Dh,0
  szEqual db Output/Input bytes are equal.,0Ah,0Dh,0
  szChange db Output/Input bytes changed.,0Ah,0Dh,0
  SECTION .text
  main:
  push dword szText1
  call printf
  pop ecx
  enable_IO:
  push word 1 ; enable mode
  push dword 04h ; four ports
  push dword 40h ; start with port 40
  call ioperm ; Must be SUID "root" for this call!
  add ESP, 10 ; cleanup stack (method 1)
  cmp eax, 0 ; check ioperm() results
  jne Error
  ;---------------------------------------Port Programming Part--------------
  SetControl:
  mov al, 96 ; R/W low byte of Counter2, mode 3
  out 43h, al ; port 43h = control register
  WritePort:
  mov bl, 0EEh ; value to send to speaker timer
  mov al, bl
  out 42h, al ; port 42h = speaker timer
  ReadPort:
  in al, 42h
  cmp al, bl ; byte should have changed--this IS a timer
  jne ByteChanged
  BytesEqual:
  push dword szEqual
  call printf
  pop ecx
  jmp disable_IO
  ByteChanged:
  push dword szChange
  call printf
  pop ecx
  ;---------------------------------------End Port Programming Part----------
  disable_IO:
  push dword szText2
  call printf
  pop ecx
  push word 0 ; disable mode
  push dword 04h ; four ports
  push dword 40h ; start with port 40h
  call ioperm
  pop ecx ;cleanup stack (method 2)
  pop ecx
  pop cx
  cmp eax, 0 ; check ioperm() results
  jne Error
  jmp Exit
  Error:
  push dword szError
  call printf
  pop ecx
  Exit:
  ret
  ; EOF ======================================================================

在 Linux 下使用中斷
  -------------------------
  Linux 是一個執行在保護模式下的共享庫的環境, 意味著沒有中斷服務, Right?
  錯了. 我注意到在 GAS 的例子原始碼中用了 INT 80, 註釋是 "sys_write(ebx, ecx, ed
  x)".
  這個函式是 Linux 系統呼叫介面的一部分, 意思是 INT 80 必須是到達系統呼叫服務
  的門戶. 在 Linux 原始碼中到處看時(忽略從不要使用 INT 80 介面的警告, 因為函式號

  可能隨時改變), 我發現 "系統呼叫號(system call numbers)" -- 就是說, 傳給 INT
  80
  的 # 對應著一個系統呼叫子程式 -- 在 UNISTD.H 中. 一共有 189 個, 所以我不會在

  這裡列出來...但如果你在 Linux 做彙編, 給自己做個好事, 列印出來吧.
  當呼叫 INT 80 時, eax 設為用呼叫的功能號. 傳給系統呼叫則程式的引數必須按順序

  放在下列暫存器中:
  ebx, ecx, edx, esi, edi
  這樣, 第一個引數就在 ebx 裡, 第二個在 ecx 裡... 注意在一個系統呼叫程式裡, 不
  是
  用棧來傳遞引數. 呼叫的返回值在 eax 裡.
  還有, INT 80 介面和一般的呼叫一樣. 下面的這個程式就演示了 INT 80h 的使用. 這
  個
  程式檢查並顯示了它自己的 PID. 注意 使用 printf() 格式化字串 -- 這個呼叫的
  C 結構
  是:
  printf( "%d ", curr_PID);
  也要注意結束符在彙編裡不一定可靠, 我常用十六進位制(0Ah, 0Dh)代表 CRLF.
  ;pid.asm====================================================================

  BITS 32
  GLOBAL main
  EXTERN printf
  SECTION .data
  szText1 db Getting Current Process ID...,0Ah,0Dh,0
  szDone db Done!,0Ah,0Dh,0
  szError db Error in int 80!,0Ah,0Dh,0
  szOutput db \%d,0Ah,0Dh,0 ;printf() 的格式字串
  SECTION .text
  main:
  push dword szText1 ;開始資訊
  call printf
  pop ecx
  GetPID:
  mov eax, dword 20 ; getpid() 系統呼叫
  int 80h ; 系統呼叫中斷
  cmp eax, 0 ; 沒有 PID 0 !
  jb Error
  push eax ; 把返回值傳遞給 printf
  push dword szOutput ; 把格式字串傳遞給 printf
  call printf
  pop ecx ; 清除棧
  pop ecx
  push dword szDone ; 結束資訊
  call printf
  pop ecx
  jmp Exit
  Error:
  push dword szError
  call printf
  pop ecx
  Exit:
  ret
  ; EOF =====================================================================
  最後的話
  -----------
  大多數的麻煩來自對 Nasm 的習慣上. 而 nasm 帶有手冊, 但預設是不安裝的,
  所以你必須把它從
  /user/local/bin/nasm-0.97/nasm.man
  移(cp 或 mv)到
  /usr/local/man/man1/nasm.man.
  格式有點亂, 可以很簡單的用 nroff 指示符來解決. 但它不會給你 Nasm 的整個文
  檔; 要解決這個問題, 把 nasmdoc.txt 從
  /usr/local/bin/nasm-0.97/doc/nasmdoc.txt
  複製到
  /usr/local/man/man1/nasmdoc.man
  現在你可以用 man nasm, man nasmdoc 來看 nasm 的手冊和文件了
  想得到更多的資訊, 查查這裡:
  Linux Assembly Language HOWTO (Linux 組合語言 HOWTO)
  Linux I/O Port Programming Mini-HOWTO (Linux I/O 埠程式設計 Mini-HOWTO)
  Jans Linux & Assembler HomePage (
  我也要感謝 Jeff Weeks(, 在我找到 Jan 的網頁之前
  給了我一些 GAS 的 hello-world 程式碼。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617542/viewspace-947282/,如需轉載,請註明出處,否則將追究法律責任。

相關文章