1. 程式人生 > >AT&T彙編學習總結二-組合語言程式範例

AT&T彙編學習總結二-組合語言程式範例

第四章組合語言範例
  1. 建立簡單程式

    • CPUID指令:CPUID指令是一條彙編指令,不容易從高階語言應用程式執行它。它是請求處理器的特定資訊並且把資訊返回到特定暫存器中的低階指令。
    • CPUID指令使用單一的暫存器值作為輸入。EAX暫存器用於決定CPUID指令生成什麼資訊,根據EAX暫存器的值,CPUID指令在EBX和EDX暫存器中生成關於處理器的不同資訊。資訊以一系列位值和標誌的形式返回。

    • CPUID指令可用的不同輸出選項:

EAX值 CPUID輸出
0 廠商ID字串和支援的最大CPUID選項值
1 處理器型別、系列、型號、和分佈資訊
2 處理器快取配置
3 處理器序列號
4 快取配置(執行緒數量、核心數量和物理屬性)
5 監視資訊
80000000h 擴充套件的廠商ID字串和支援的級別
80000001h 擴充套件的處理器型別、系列、型號和分佈資訊
80000002h - 80000x4h 擴充套件的處理器名稱字串

2. 範例程式

  • .ascii宣告ASCII字串宣告一個文字字串,字串元素被預訂以並且放在記憶體中,其起始記憶體位置由標籤output指示:
output:
    .ascii "The processor Vendor ID is 'xxxxxxxxxx'\n"
  • 宣告程式的指令碼段和一般的起始標籤:
.section .text
.globl _start
_start:
  • EAX暫存器載入零值,然後執行CPUID指令:
movl $0, %eax
cpuid
  • EDI是用於字串操作的目標的資料指標,EAX中的零值定義CPUID輸出選項。CPUID執行後,必須收集分散在3個輸出暫存器中的指令響應:
movl $output %edi
movl %ebx, 28(%edi)
movl %ebx, 32(%edi)
movl %ebx, 36(%edi)
  • EAX:用於運算元和結果資料的累加器;EBX:指向資料記憶體段中的資料的指標;EDX:I/O指標;ECX:字串和迴圈操作的計數器;EDI:用於字串操作的目標的資料指標。
    第一條指令建立一個指標,處理記憶體中宣告的output變數時會使用這個指標。output標籤的記憶體位置被載入到EDI暫存器中。括號外的數字表示相對於output標籤的放置資料的位置,這個數字和EDI暫存器中的地址相加,確定暫存器的被寫入的地址
    ,按照EDI指標、包括廠商ID字串片段的3個暫存器的內容被放到資料記憶體中的正確位置:
movl $4, %eax
movl $1, %ebx
movl $output, %ecx
movl $42, %edx
int $0x80
  • 這個程式使用一個Linux系統呼叫(inti $0x80)從Linux核心訪問控制檯顯示:
movl $4, %eax
movl $1, %ebx
movl $output, %ecx
movl $42, %edx
int $0x80
  1. 構建可執行程式

    • 使用GNU彙編器和GNU聯結器構建可執行程式,第一步使用as命令把組合語言原始碼彙編為目的碼檔案cpuid.o,第二步使用ld把目的碼檔案連線為可執行檔案cpuid:
as -o cpuid.o cpuid.s
ld -o cpuid cpuid.o
  1. 執行可執行程式

    • Linux的好處之一是一些版本能夠執行在大多數可能已經被放到一邊不用的老型號計算機上。
      ./cpuid
  2. 使用匯編器進行彙編

    • gcc彙編程式時有一個問題,GNU聯結器查詢_start標籤以便確定程式的開始的位置,但是gcc查詢的是main標籤。所以必須把程式中的_start標籤和定義標籤的.globl命令都改成下面:
.section .txt
.globl main
main:
  1. 除錯程式

    • 在更加複雜的程式中,在給暫存器和記憶體位置賦值或者試圖使用特定指令碼處理複雜資料事物時,易錯誤。為除錯組合語言程式,首先必須使用-gstabs引數重新彙編原始碼:
    • 因為-gstabs引數在可執行程式檔案中添加了附加資訊,所以產生的檔案比僅僅執行應用程式所需的檔案要大一些,所以若非必要就不要使用除錯資訊
as -gstabs -o cpuid.o cpuid.s
ld -o cpuid cpuid.o
  1. 除錯程式之設定斷點,在下列任何情況下停止程式的執行:
    • 到達某個標籤
    • 到達原始碼中的某個行號
    • 函式執行了指定的次數之後
      break命令格式:其中label是被引用的原始碼中的標籤,offset是執行應該停止的地方距離這個標籤的行數。
break * label+offset
**舉例:這裡使用\*_start引數指定了斷點,這個引數指定_start標籤後面的第一條指令碼,但不幸的是,當程式執行時,它會忽略這個斷點並且執行完整個程式。這是gdb當前版本中眾知的一個缺點:**
break *_start
  • 解決方法是在*_start標籤後的第一個指令碼元素的位置包含一條偽指令,在組合語言中,空指令稱為NOP,意思是空操作:
_start:
    nop
    movl $0, %eax
    cpuid

在新增NOP指令後,就可以在這個位置建立斷電,表示_start+1

break *_start+1
  1. 除錯程式之檢視資料
    • 兩種最常被檢查的資料元素是用於變數的暫存器和記憶體位置:
資料命令 描述
info registcrs 顯示所有暫存器的值
print 顯示特定暫存器或者來自程式的變數的值
x 顯示特定記憶體位置的內容

* printf命令也可以用於顯示各個暫存器的值,加上一個修飾符就可以修改print命令輸出格式:

print
print/d顯示十進位制的值
print/t顯示二進位制的值
print/x顯示十六進位制的值

* 使用print命令

print/x $ebx
  • x命令用於顯示特定記憶體位置的值,和print命令類似,可以使用修飾符修改x命令的輸出。x命令格式是:
x/nyz
n是顯示欄位數 y是輸出格式 z是要顯示的欄位的長度
c用於字元 b用於位元組
d用於十進位制 h用於十六位字(半字)
x用於十六進位制 w用於32位字

9. 在組合語言中使用C庫函式
* 開頭使用的output裡使用的是.aasciz命令,而不是.ascii。printf函式要求以空字元結尾的字串作為輸出字串。.asciz命令在定義的字串末尾新增空字元。

output:
.asciz "The processor Vendor ID is '%s'\n"

  • 使用.lcomm命令,使用以下引數是將包含廠商ID字串的緩衝區。因為不需要定義緩衝區的值,所以在bss段中使用.lcomm命令把它宣告為12位元組的緩衝區區域:
.section .bss
    .lcomm buffer, 12
  • 把引數傳遞給C函式printf,必須把他們壓入堆疊。這是使用PUSHL指令完成。引數放入堆疊的順序和printf函式獲取他們的順序是相反的,所以緩衝區值被首先放入,然後是輸出字串值,最後使用CALL指令呼叫printf函式,ADDL指令用於清空為printf函式放入堆疊的引數:

    pushl $buffer
    pushl $output
    call printf
    addl $8, %esp

    1. 連線C庫函式
  • 第一種方式為靜態連結:靜態連結把函式目的碼直接連線到應用程式的可執行程式檔案中。會造成建立巨大的可執行程式、並且如果同時執行程式的多個例項,就會造成記憶體浪費(每個例項都有其自己的相同函式的拷貝)。
  • 第二種方式為動態連線:動態連線使用庫的方式使程式設計師可以在應用程式中引用函式,但是不把函式程式碼連線到可執行程式檔案。在程式執行時由作業系統呼叫動態連結庫,並且多個程式可以共享動態連結庫。
    Linux中標準C動態庫位於libc.so.x檔案中,使用-l引數時不用指定完整庫名稱

    ld -o cpuid -lc cpuid.o
  • 必須指定在執行時載入動態庫的程式,在linux系統中,這個程式是ld-linux.so.2,通常在/lib下,為了指定這個程式,必須使用GNU聯結器的-dynamic-linker引數

ld -dynamic-linker /lib/ld-linux.so.2 -o cpuid -lc cpuid.o
./cpuid