X86處理器彙編技術系列4

第23部分- Linux x86 64位彙編 字符串存儲加載

除了字符串從一個內存位置傳送到另一個內存位置外,還有用於把內存中的字符串值加載到寄存器以及傳會至內存位置中的指令。

lods

lods指令把內存中的字符串值傳送到EAX寄存器中。

有多種格式:lodsb, lodsw, lodsl,lodsq

使用ESI寄存器作爲隱藏的源操作數。

 

stos

STOS指令可以把EAX寄存器中的數據存放到一個內存位置中。

有STOSB,STOSW,STOSL,STOSQ。

使用EDI寄存器作爲隱含的目標操作數。

 

加載示例

 
  1. .extern printf ;//調用外部的printf函數
  2. .section .data
  3. space:
  4. .ascii "h"
  5. oa:
  6. .byte 0x0a
  7. end:
  8. .byte 0
  9. .section .bss
  10. .lcomm stobuffer, 256
  11. .section .text
  12. .globl _start
  13. _start:
  14. nop
  15. leal space, %esi
  16. leal stobuffer, %edi
  17. movl $255, %ecx
  18. cld
  19. lodsb
  20. rep stosb
  21.  
  22. leal oa, %esi
  23. lodsb
  24. stosb
  25. leal end, %esi
  26. lodsb
  27. lodsb
  28.  
  29. movq $stobuffer,%rdi
  30. call printf
  31.  
  32. movq $60,%rax
  33. syscall
 

as -g -o stostest.o stostest.s

ld -o stostest stostest.o -lc -I /lib64/ld-linux-x86-64.so.2

在這裏打印的時候,需要注意的是由於引入了printf函數,該函數指向的buffer的指針是48位地址的bss地址,不是32位的。

第24部分- Linux x86 64位彙編 字符串比較

比較字符串可以使用CMPS指令系列。

CMPSB,CMPSW,CMPSL,CMPSQ。

隱含的源和目標操作數的位置同樣存儲在ESI和EDI寄存器中。每次執行CMPS指令時,根據DF標誌的設置。

CMPS指令從源字符串中減去目標字符串,並且適當地設置EFLAGS寄存器的進位、符號、溢出、零、奇偶校驗和輔助進位。使用之後可以使用跳轉指令跳轉到分支。

         REP指令也可以用於跨越多個字節重複進行字符串比較。由於REP指令不在兩個重複的過程之間檢查標誌的狀態,需要REPE,REPNE,REPZ,REPNZ,這些指令在每次重複過程中檢查零標誌,如果零標誌被設置就停止重複。遇到不匹配的字符對,REP指令就會停止重複。

     最常用於比較字符串的方法稱爲詞典式順序,按照字典對此進行排序。

  • 按字母表順序,較低的字母小於較高的字母
  • 大寫字母小於小寫字母

對於長短 不一的字符串,當短字符串等於長字符串中相同數量的字符,那麼長字符串就大於短字符串。例如:test小於test1

 

比較示例

 
  1. .extern printf ;//調用外部的printf函數
  2. .section .data
  3. string1:
  4. .ascii "test1"
  5. length1:
  6. .int 5
  7. string2:
  8. .ascii "test1"
  9. length2:
  10. .int 5
  11. big:
  12. .ascii "big"
  13. .byte 0x0a,0x00
  14.  
  15. small:
  16. .ascii "small"
  17. .byte 0x0a,0x00
  18.  
  19. equ:
  20. .ascii "equal"
  21. .byte 0x0a,0x00
  22.  
  23. .section .text
  24. .globl _start
  25. _start:
  26. nop
  27. lea string1, %esi
  28. lea string2, %edi
  29. movl length1, %ecx
  30. movl length2, %eax
  31. cmpl %eax, %ecx;//比較兩個字符串長度,短的加載到寄存器,供REPE使用
  32. ja longer;//如果字符串2長跳轉到longer,不用交換
  33. xchg %ecx, %eax;//否則,進行交換
  34. longer:
  35. cld
  36. repe cmpsb
  37. je equal;//最小長度下都相等,需要比較字符串長度。
  38. jg greater
  39.  
  40. less:
  41. movq $small,%rdi
  42. call printf
  43. movq $60,%rax
  44. syscall
  45.  
  46. greater:
  47. movq $big,%rdi
  48. call printf
  49. movq $60,%rax
  50. syscall
  51.  
  52. equal:;// 比較字符串長度。
  53. movl length1, %ecx
  54. movl length2, %eax
  55. cmpl %ecx, %eax
  56. jg greater
  57. jl less
  58. movq $equ,%rdi
  59. call printf
  60.  
  61. movq $60,%rax
  62. syscall
 

as -g -o strcomp.o strcomp.s

ld -o strcomp strcomp.o -lc -I /lib64/ld-linux-x86-64.so.2

第25部分- Linux x86 64位彙編 字符串掃描

掃描字符串可以使用SCAS指令。提供了掃描字符串搜索特定的一個字符或者一組字符。

         SCAS指令系統包含:SCASB,SCASW,SCASL,SCASQ

使用EDI寄存器作爲隱含的目標操作數。EDI寄存器必須包含要掃描的字符串的內存地址。遞增和遞減取決於DF標誌。

         比較時,會相應的設置EFLAGS的輔助進位,進位,奇偶校驗,溢出、符號和零標誌。把EDI寄存器當前指向的字符和AL寄存器的字符進行比較,和CMPS指令類似。和REPE和REPNE前綴一起,才顯得方便。REPE和REPNE常常用於找到搜索字符時停止掃描。

      掃描字符串示例

 
  1. .extern printf ;//調用外部的printf函數
  2. .section .data
  3. string1:
  4. .ascii "This is a test - a long text string to scan."
  5. length:
  6. .int 44
  7. string2:
  8. .ascii "-"
  9. noresult:
  10. .ascii "No result."
  11. .byte 0x0a,0x0
  12. yesresult:
  13. .ascii "Yes have result."
  14. .byte 0x0a,0x0
  15. .section .text
  16. .globl _start
  17. _start:
  18. nop
  19. leal string1, %edi
  20. leal string2, %esi
  21. movl length, %ecx
  22. lodsb;//加載第一個-到al寄存器
  23. cld
  24. repne scasb;//掃描字符串string1,知道找到-字符。
  25. jne notfound
  26. movq $yesresult,%rdi
  27. call printf
  28.  
  29. movq $60,%rax
  30. syscall
  31. notfound:
  32. movq $noresult,%rdi
  33. call printf
  34.  
  35. movq $60,%rax
  36. syscall
 

as -g -o scastest.o scastest.s

ld -o scastest scastest.o -lc -I /lib64/ld-linux-x86-64.so.2

搜索字符示例

Scasw和scasl可以用於搜索2個或4個字符,查找AX或者EAX寄存器中的字符序列。

 
  1. .extern printf ;//調用外部的printf函數
  2. .section .data
  3. string1:
  4. .ascii "This is a test - a long text string to scan."
  5. length:
  6. .int 11
  7. string2:
  8. .ascii "test"
  9. noresult:
  10. .ascii "No result."
  11. .byte 0x0a,0x0
  12. yesresult:
  13. .ascii "Yes have result."
  14. .byte 0x0a,0x0
  15.  
  16. .section .text
  17. .globl _start
  18. _start:
  19. nop
  20. leal string1, %edi
  21. leal string2, %esi
  22. movl length, %ecx
  23. lodsl;//加載test到eax寄存器
  24. cld
  25. repne scasl;//進行比較知道找到位置
  26. jne notfound
  27. subw length, %cx
  28. neg %cx
  29.  
  30. movq $yesresult,%rdi
  31. call printf
  32.  
  33. movq $60,%rax
  34. syscall
  35.  
  36. notfound:
  37. movq $noresult,%rdi
  38. call printf
  39.  
  40. movq $60,%rax
  41. syscall
 

as -g -o scastest.o scastest2.s

ld -o scastest scastest.o -lc -I /lib64/ld-linux-x86-64.so.2

這裏沒找到test,是因爲11次對比的方式是這樣的,一次4個字節,打亂了原先的test。

將test改成其他圖中的4個字符就可以找到了。

 

計算字符串長度示例

SCAS指令有一個功能是確定零結尾(也稱爲空結尾)的字符串長度。

搜索零的位置,找到後就可以會到總共搜索了多少個字符,也就是字符的長度。

 
  1. .extern printf ;//調用外部的printf函數
  2. .section .data
  3. string1:
  4. .asciz "Testing, one, two, three, testing.\n"
  5.  
  6. noresult:
  7. .ascii "No Result."
  8. .byte 0x0a,0x0
  9. yesresult:
  10. .ascii "Yes have result : %d."
  11. .byte 0x0a,0x0
  12.  
  13. .section .text
  14. .globl _start
  15. _start:
  16. nop
  17. leal string1, %edi
  18. movl $0xffff, %ecx;//假設字符串長65535
  19. movb $0, %al
  20. cld
  21. repne scasb
  22. jne notfound
  23. subw $0xffff, %cx;//減去初始值,變成了負值
  24. neg %cx;//取反就是正數了
  25. dec %cx;//去掉最後一個空符號,就是字符串長度了。
  26.  
  27. movq $yesresult,%rdi
  28. movq %rcx,%rsi
  29. call printf
  30.  
  31. movq $60,%rax
  32. syscall
  33.  
  34. notfound:
  35. movq $noresult,%rdi
  36. call printf
  37.  
  38. movq $60,%rax
  39. syscall
 

as -g -o strsize.o strsize.s

ld -o strsize strsize.o -lc -I /lib64/ld-linux-x86-64.so.2

第26部分- Linux x86 64位彙編 字符串操作反轉實例

有了上面幾個深入的語法知識後,我們繼續來學習一個例子。

我們出兩個版本的例子,實現字符串的反轉效果。

AT&T彙編實現

將上訴的Intel彙編程序,我們改寫成AT&T語法後彙編如下,這裏將原來的mov rdi, $ + 15地方進行的修改,變成jmp命令了,jmp過去jmp回來,就不用涉及到棧中的函數地址了。

因爲NASM 提供特殊的變量($ 和 $$ 變量)來操作位置計數器。在 GAS 中,無法操作位置計數器,必須使用標籤計算下一個存儲位置(數據、指令等等)。

 
  1. .section .data
  2. .equ SYS_WRITE,1
  3. .equ STD_OUT,1
  4. .equ SYS_EXIT,60
  5. .equ EXIT_CODE,0
  6. .equ NEW_LINE,0xa
  7. INPUT: .ascii " Hello world!\n"
  8. .section .bss;;//定義buffer緩存
  9. .lcomm OUTPUT,12
  10. .global _start
  11. .section .text
  12. _start:
  13. movq $INPUT ,%rsi;//將字符串地址賦值爲rsi
  14. xor %rcx, %rcx;//清零
  15. cld;//清空df標誌位0
  16.  
  17. jmp calculateStrLength;//調用函數計算字符串長度
  18. reversePre:
  19. xor %rax, %rax;//清空rax
  20. xor %rdi, %rdi;//清空rdi
  21. jmp reverseStr;//跳轉到reverseStr函數進行翻轉
  22.  
  23. calculateStrLength:;//計算INPUT字符串的長度,並保存在rcx中。
  24. cmpb $0 ,(%rsi);//0對比,字符串最後一個’\0’字符串結束標誌
  25. je exitFromRoutine;//函數結束
  26. lodsb;//從rds:rsi處加載一個字節到al,並增加rsi
  27. pushq %rax;/將剛獲取到的al進行壓棧,不過這會破壞了原本返回地址的位置。返回使用通過push rdi來解決。
  28. inc %rcx;//增加計數器
  29. jmp calculateStrLength;//循環
  30. exitFromRoutine:;//返回到_start主線。
  31. pushq %rdi;//壓棧return地址到棧中
  32.  
  33. jmp reversePre;
  34. reverseStr:
  35. cmpq $0,%rcx;//對比0,rcx已保存的是字符串的長度
  36. je printResult;//相等則跳轉到printResult
  37. popq %rax;//從棧中彈出一個字符給rax
  38. movq %rax,OUTPUT(%rdi);//寫到[OUTPUT+rdi]對應的地址
  39. dec %rcx;//寫一個少一個
  40. inc %rdi;//寫一個地址加一個
  41. jmp reverseStr;//返回循環
  42. printResult:
  43. mov %rdi, %rdx;//輸出的長度賦值到rdx
  44. mov $SYS_WRITE,%rax;//調用號
  45. mov $STD_OUT, %rdi;//輸出句柄
  46. mov $OUTPUT, %rsi;//輸出字符串地址
  47. syscall;//調用sys_write輸出OUTPUT保存的反向字符串。
  48. jmp printNewLine;//調用函數輸出換行符
  49.  
  50. printNewLine:
  51. mov $SYS_WRITE, %rax;//調用號
  52. mov $STD_OUT, %rdi;//輸出句柄
  53. mov $NEW_LINE, %rsi;//輸出字符串地址
  54. mov $1,%rdx;//輸出的長度賦值到rdx
  55. syscall
  56. jmp exit
  57.  
  58. exit:
  59. mov $SYS_EXIT ,%rax
  60. mov $EXIT_CODE ,%rdi
  61. syscall
 

編譯:

as -g -o reverse_att.o reverse_att.s

ld  -o reverse_att reverse_att.o

Intel彙編實現

 
  1. section .data
  2. SYS_WRITE equ 1
  3. STD_OUT equ 1
  4. SYS_EXIT equ 60
  5. EXIT_CODE equ 0
  6.  
  7. NEW_LINE db 0xa
  8. INPUT db "Hello world!"
  9. section .bss;;//定義buffer緩存
  10. OUTPUT resb 12
  11. global _start
  12. section .text
  13. _start:
  14. mov rsi, INPUT;//將字符串地址賦值爲rsi
  15. xor rcx, rcx;//清零
  16. cld;//清空df標誌位0
  17. mov rdi, $ + 15;//預處理rdi寄存器,在calculateStrLength函數中會用到。$是表示當前指令的地址。$$是當前段的地址。通過$+15可以計算得到命令xor rax,rax這條指令的地址(可以通過objdump -D命令來數出來),將該地址賦值給rdi進行了保存。在calculateStrLength函數中可以直接獲取rdi地址來返回到xor rax,rax命令。
  18. call calculateStrLength;//調用函數計算字符串長度
  19. xor rax, rax;//清空rax
  20. xor rdi, rdi;//清空rdi
  21. jmp reverseStr;//跳轉到reverseStr函數進行翻轉
  22.  
  23. calculateStrLength:;//計算INPUT字符串的長度,並保存在rcx中。
  24. cmp byte [rsi], 0;//0對比,字符串最後一個’\0’字符串結束標誌
  25. je exitFromRoutine;//函數結束
  26. lodsb;//從rds:rsi處加載一個字節到al,並增加rsi
  27. push rax;/將剛獲取到的al進行壓棧,不過這會破壞了原本返回地址的位置。返回使用通過push rdi來解決。
  28. inc rcx;//增加計數器
  29. jmp calculateStrLength;//循環
  30. exitFromRoutine:;//返回到_start主線。
  31. push rdi;//壓棧return地址到棧中
  32. ret
  33. reverseStr:
  34. cmp rcx, 0;//對比0,rcx已保存的是字符串的長度
  35. je printResult;//相等則跳轉到printResult
  36. pop rax;//從棧中彈出一個字符給rax
  37. mov [OUTPUT + rdi], rax;寫到[OUTPUT+rdi]對應的地址
  38. dec rcx;//寫一個少一個
  39. inc rdi;//寫一個地址加一個
  40. jmp reverseStr;//返回循環
  41.  
  42. printResult:
  43. mov rdx, rdi;//輸出的長度賦值到rdx
  44. mov rax, SYS_WRITE;//調用號
  45. mov rdi, STD_OUT;//輸出句柄
  46. mov rsi, OUTPUT;//輸出字符串地址
  47. syscall;//調用sys_write輸出OUTPUT保存的反向字符串。
  48. jmp printNewLine;//調用函數輸出換行符
  49.  
  50. printNewLine:
  51. mov rax, SYS_WRITE;//調用號
  52. mov rdi, STD_OUT;//輸出句柄
  53. mov rsi, NEW_LINE;//輸出字符串地址
  54. mov rdx, 1;//輸出的長度賦值到rdx
  55. syscall
  56. jmp exit
  57.  
  58. exit:
  59. mov rax, SYS_EXIT
  60. mov rdi, EXIT_CODE
  61. syscall
 

編譯和鏈接:

nasm -g -f elf64 -o reverse.o reverse.asm

ld -o reverse reverse.o

執行後輸出如下:

# ./reverse

!dlrow olleH

第27部分- Linux x86 64位彙編 寄存器

64位時候X86處理器的寄存器如下圖:

《Computer Systems A Programmer's Perspective, 3rd Edition》文件中有這圖。re

64和32位的差異是:

  • 64位有16個寄存器,32位只有8個。但是32位前8個都有不同的命名,分別是e開頭,而64位前8個使用了r代替e。e開頭的寄存器命名依然可以直接運用於相應寄存器的低32位。而剩下的寄存器名則是從r8 - r15,其低位分別用d,w,b指定長度。
  • 32位使用棧幀來作爲傳遞的參數的保存位置,而64位使用寄存器,分別用rdi,rsi,rdx,rcx,r8,r9作爲第1-6個參數。rax作爲返回值
  • 64位沒有棧幀的指針,32位用ebp作爲棧幀指針,64位取消了這個設定,rbp作爲通用寄存器使用
  • 64位支持一些形式的以PC相關的尋址,而32位只有在jmp的時候纔會用到這種尋址方式。

第28部分- Linux x86 64位彙編 宏定義和函數

在前面的例子移植中,我們知道NASM 使用 resb、resw 和 resd 關鍵字在 BSS 部分中分配字節、字和雙字空間。GAS 使用 .lcomm 關鍵字分配字節級空間。

Gas彙編器宏

Linux 平臺的標準彙編器是 GAS。

GAS 提供 .macro 和 .endm 指令來創建宏。.macro 指令後面跟着宏名稱,後面可以有參數,也可以沒有參數。宏參數是按名稱指定的。

 
  1. .macro write_string p1,p2
  2. movq $1,%rax;//調用號sys_write
  3. movq $1,%rdi;//輸出到stdout
  4. movq \p1,%rsi;//字符串地址
  5. movq \p2,%rdx;//字符串長度
  6. syscall
  7. .endm
  8.  
  9. .section .data
  10. msg1: .ascii "Hello,programmers!\n"
  11. len1: .quad len1-msg1
  12.  
  13. msg2: .asciz "Welcome to the world of,\n"
  14. len2: .quad len2-msg2
  15.  
  16. msg3: .asciz "Linux assembly programming!\n"
  17. len3: .quad len3-msg3
  18.  
  19. .section .text
  20. .global _start
  21. _start:
  22. write_string $msg1,len1;//第一個參數是字符串地址,第二個參數是字符串長度不是其地址。
  23. write_string $msg2,len2
  24. write_string $msg3,len3
  25.  
  26. movq $1,%rax ;//系統調用號(sys_exit)
  27. int $0x80 ;//調用內核
 

 

編譯:

as -g -o macro_att.o macro_att.s

ld  -o macro_att macro_att.o

Gas數據結構

數據結構如下:

 
  1. .extern printf ;//調用外部的printf函數
  2. .section .data
  3. fmt: .ascii "name = %s, high = %ld \n"
  4.  
  5. .set name_size,20
  6. .set high_size,4
  7.  
  8. people:
  9. name: .space name_size
  10. high: .space high_size
  11.  
  12. xiaoming:
  13. xm_name: .ascii "xiaoming"
  14. xm_null: .byte 0
  15. xm_high: .quad 0xaf
  16.  
  17. .section .text
  18. .global _start
  19. _start:
  20. movq $fmt, %rdi
  21. movq $xm_name,%rsi
  22. movq xm_high, %rdx
  23. movq $0, %rax
  24. call printf
  25.  
  26. movq $60, %rax
  27. syscall
 

as -g -o struc_att.o struc_att.s

ld  -o struc_att struc_att.o -lc -I /lib64/ld-linux-x86-64.so.2

同樣gas也可以使用.include來包含其他文件。

 

Nasm彙編器宏和外部函數

單行宏定義如下:

%define macro_name(parameter) value

可以看到NASM 使用 %begin macro 指令聲明宏。

多行宏定義以%macro指令開頭,以%endmacro結尾。

%macro macro_name  number_of_params
<macro body>
%endmacro

 

宏名稱後面是一個數字,是宏需要的宏參數數量。在 NASM 中,宏參數是從 1 開始連續編號的。宏的第一個參數是 %1,第二個是 %2,第三個是 %3,以此類推。

例子代碼如下:

 
  1. ; A macro with two parameters
  2. ; Implements the write system call
  3. %macro write_string 2
  4. mov rax, 4
  5. mov rbx, 1
  6. mov rcx, %1
  7. mov rdx, %2
  8. int 80h
  9. %endmacro
  10.  
  11. section .text
  12. global _start ;must be declared for using gcc
  13.  
  14. _start: ;tell linker entry point
  15. write_string msg1, len1
  16. write_string msg2, len2
  17. write_string msg3, len3
  18.  
  19. mov rax,1 ;system call number (sys_exit)
  20. int 0x80 ;call kernel
  21.  
  22. section .data
  23. msg1 db 'Hello, programmers!',0xA,0xD
  24. len1 equ $ - msg1
  25.  
  26. msg2 db 'Welcome to the world of,', 0xA,0xD
  27. len2 equ $- msg2
  28.  
  29. msg3 db 'Linux assembly programming! ',0xA,0xD
  30. len3 equ $- msg3
 

nasm -g -f elf64 -o macro.o macro.asm

ld -o macro macro.o

Nasm結構使用

在C語言中可以使用struct聲明結構體,而在nasm彙編中,也可以使用結構體,通過使用僞指令來聲明結構體。

使用數據結構例子如下,還實現了外部函數的調用:

 
  1. .extern printf ;//調用外部的printf函數
  2. .section .data
  3. fmt: .ascii "name = %s, high = %ld \n"
  4.  
  5. .set name_size,20
  6. .set high_size,4
  7.  
  8. people:
  9. name: .space name_size
  10. high: .space high_size
  11.  
  12. xiaoming:
  13. xm_name: .ascii "xiaoming"
  14. xm_null: .byte 0
  15. xm_high: .quad 0xaf
  16.  
  17. .section .text
  18. .global _start
  19. _start:
  20. movq $fmt, %rdi
  21. movq $xm_name,%rsi
  22. movq xm_high, %rdx
  23. movq $0, %rax
  24. call printf
  25.  
  26. movq $60, %rax
  27. syscall
 

nasm -g -f elf64 -o struc.o struc.asm

ld -o struc struc.o -lc -I /lib64/ld-linux-x86-64.so.2

也可以使用%include來包含其他彙編文件。

此外,還可以有條件預編譯宏如下:
    %ifdef _BOOT_DEBUG
         org 0100h
    %else
         org 07c00h
    %endif

第29部分-Linux x86 64位彙編 加法指令

adc指令可以執行兩個無符號或者帶符號整數值的加法,並且把前一個ADD指令產生的進位標誌的值包含在其中。爲了執行多組多字節的加法操作,可以把多個ADC指令鏈接在一起。

示例

代碼如下:

 
  1. .section .data
  2. data1:
  3. .quad 11111111,7252051615;//16字節整數
  4. data2:
  5. .quad 22222222,5732348928;//16字節整數
  6. output:
  7. .asciz "The result is %qd\n"
  8. .section .text
  9. .globl _start
  10. _start:
  11. movq data1, %rbx;//保存data1的前8個字節到ebx
  12. movq data1+8, %rax;//保存data1的後8個字節到ebx
  13. movq data2, %rdx;//保存data2的前8個字節到ebx
  14. movq data2+8, %rcx;//保存data2的後8個字節到ebx
  15. addq %rbx, %rdx;//8個字節相加
  16. adcq %rcx, %rax;//8個字節相加
  17. adcq %rdx, %rax;//全部相加
  18. movq $output,%rdi
  19. movq %rax ,%rsi
  20. call printf
  21. movq $60,%rax
  22. movq $0,%rdi
  23. syscall
 

as -o adctest.o adctest.s

ld -o adctest adctest.o -lc -I /lib64/ld-linux-x86-64.so.2

減法也是類型,減法中有類似adc的功能,是sbb指令。

第30部分-Linux x86 64位彙編 乘法/除法

乘法

無符號整數乘法mul如下,目標操作數總是eax寄存器的某種形式。

使用IMUL可以進行有符號乘法。

在只有一個操作數的情況下,結果保存到指定的目標寄存器EAX或寄存器的某種形式,這個同MUL。

IMUL支持2個操作數。

imul source,destination

IMUL支持3個操作數。

imul multiplier,source,destination

其中multiplier是一個立即值。

乘法實例

 
  1. .section .data
  2. output:
  3. .asciz "The result is %d, %ld\n"
  4. value1:
  5. .int 10
  6. value2:
  7. .int -35
  8. value3:
  9. .int 400
  10. .section .text
  11. .globl _start
  12. _start:
  13. nop
  14. movl value1, %ebx;//移動value1到ebx
  15. movl value2, %ecx;//移動value2到ecx
  16. imull %ebx, %ecx;//將ebx和ecx相乘結果保存到ecx
  17. movl value3, %edx;//移動value3到edx
  18. imull $2, %edx, %eax;//繼續相差
  19. movq $output,%rdi
  20. movl %ecx ,%esi;//
  21. movl %eax ,%edx;//餘數
  22. call printf
  23. movl $1, %eax
  24. movl $0, %ebx
  25. int $0x80
 

as -g -o imultest.o imultest_att.s

ld -o imultest imultest.o -lc -I /lib64/ld-linux-x86-64.so.2

 

除法

除法也和乘法類似。無符號除法使用div,有符號是idiv。

不過idiv只有一個參數值。

除法操作如下:

除法實例

 
  1. .section .data
  2. dividend:
  3. .quad 8335
  4. divisor:
  5. .int 25
  6. quotient:
  7. .int 0
  8. remainder:
  9. .int 0
  10. output:
  11. .asciz "<93>The quotient is %d, and the remainder is %d\n<94>"
  12. .section .text
  13. .globl _start
  14. _start:
  15. nop
  16. movl dividend, %eax
  17. movl dividend+4, %edx
  18. divw divisor ;//除數
  19. movl %eax, quotient
  20. movl %edx, remainder
  21.  
  22. movq $output,%rdi
  23. movl quotient,%esi;//
  24. movl remainder,%edx;//餘數
  25.  
  26. call printf
  27. movl $1, %eax
  28. movl $0, %ebx
  29. int $0x80
 

as -g -o divtest.o divtest.s

ld -o divtest divtest.o -lc -I /lib64/ld-linux-x86-64.so.2

第31部分-Linux x86 64位彙編 移位指令

乘法和除法是處理器上最爲耗費時間的兩種指令,但是運用移位指令可以提供快速的方法。

移位乘法

爲了使整數乘以2的乘方,必須把值向左移位。可以使用兩個指令使得整數值向左移位:SAL(向左算術位移)和SHL(向左邏輯位移)。

移位除法

通過移位進行除法操作涉及把二進制值向右移位。

當把整數值向右移位時,必須要注意整數的符號。

無符號整數,向右移位產生的空位可以被填充爲零,而且不會有任何問題。

不幸的事,對於帶符號整數,使用零填充高位部分會對負數產生有害的影響。

有兩個向右移位指令,SHR指令清空移位造成的空位,只能用於對無符號整數進行移位操作。SAR指令根據整數的符號,要麼清空,要麼設置移位造成的空位。對於負數,空位被設置爲1,對於正數,被清空爲0.

循環移位

循環移位指令執行的功能和移位指令一樣,只不過溢出位被存放回值的另一端,而不是被丟棄。

第32部分-Linux x86 64位彙編 數據傳輸

無符號條件傳送指令

無符號條件傳送指令依靠進位、零和奇偶校驗標誌來確定兩個操作數之間的區別。

帶符號條件傳送指令如下:

帶符號條件傳送指令使用符號和溢出標誌表示操作數之間比較的狀態。

示例

實例如下,查找數組中一系列整數中最大的一個。

 
  1. .section .data
  2. output:
  3. .asciz "The largest value is %d\n";//定義字符串
  4. values:
  5. .int 105, 235, 61, 315, 134, 221, 53, 145, 117, 5;//定義整型
  6. .section .text
  7. .globl _start
  8. _start:
  9. nop
  10. movl values, %ebx;//ebx保存最大的整數,第一個是105
  11. movl $1, %edi;//移動計數
  12. loop:
  13. movl values(, %edi, 4), %eax;//逐個加載到eax寄存器。
  14. cmp %ebx, %eax;//和ebx比較
  15. cmova %eax, %ebx;//eax大於ebx,則將eax移動到ebx。
  16. inc %edi;//增加edi
  17. cmp $10, %edi;//是否已經對比了10個。
  18. jne loop
  19. movq $output,%rdi
  20. movq %rbx,%rsi
  21. call printf
  22. movq $60,%rax
  23. movq $0,%rdi
  24. syscall
 

as -g -o hello.o hello.s

ld -o hello hello.o -lc -I /lib64/ld-linux-x86-64.so.2

第33部分-Linux x86 64位彙編 交換數據-冒泡算法實現

數據交換指令如下:

冒泡排序示例

冒泡排序有兩個循環邏輯,內層循環遍歷數組,檢查相鄰的了兩個數組值,找出哪個更大。外層循環控制總共執行了多少次內層循環。

使用兩個計數器ebx和ecx,ebx是內層循環,ecx是外層循環。

如下:

 
  1. .section .data
  2.  
  3. values:
  4. .int 105, 235, 61, 315, 134, 221, 53, 145, 117, 5
  5. .int 0
  6. .section .text
  7. .globl _start
  8. _start:
  9. movl $values, %esi;//數組地址存放於esi
  10. movl $9, %ecx;//外層循環次數
  11. movl $9, %ebx;//內層循環次數
  12. loop:
  13. movl (%esi), %eax;//第一個值或當前數值移動給eax
  14. cmp %eax, 4(%esi) ;//第一個值或當前數值和下一個值進行比較
  15. jge skip;//如果大於等於第二個值則跳轉到skip,否則用下面兩條命令進行交換,將小的值往後冒泡
  16. xchg %eax, 4(%esi)
  17. movl %eax, (%esi)
  18. skip:
  19. add $4, %esi;//繼續指向下一個值
  20. dec %ebx;//內存循環遞減
  21. jnz loop
  22. dec %ecx;//內存循環結束,外層循環遞減
  23. jz end;//外層循環結束
  24. movl $values, %esi;//內存循環重新開始前重置esi,並設置ecx,ebx。
  25. movl %ecx, %ebx
  26. jmp loop
  27. end:
  28. movl $1, %eax
  29. movl $0, %ebx
  30. int $0x80
 

as -g -o bubble_att.o bubble_att.s

ld -o bubble_att bubble_att.o -lc -I /lib64/ld-linux-x86-64.so.2

使用gdb進行調試觀察:

gdb -q ./bubble_att

在end標記處設置斷點:

(gdb)break *end

先顯示values標記處的值:

(gdb)x /10d &values

然後啓動運行如下:

(gdb)run

最後查看values標記處,可以看到已經排好序。

(gdb)x /10d &values

第34部分-Linux x86 64位彙編 優化

內存優化

編寫高性能的彙編語言程序時,最好儘可能地避免內存訪問。最快的方式是通過寄存器,但是不可能所有應用程序數據都保存在寄存器中。

對於有數據緩存的處理器來說,內存中按照連續的順序訪問內存能夠幫助提高緩存命中率,內存塊會一次被讀取到緩存中。

         目前X86的緩衝塊就是cacheline長度是64位,如果數據元素超過64位塊必須是要次緩存操作才能獲取或者存儲內存中的數據元素。

         Gas彙編器支持.align命令,用於在特定的內存邊界對準定義的數據元素。在數據段中,.align命令緊貼在數據定義的前面,指示彙編器按照內存邊界安置數據元素。

優化分支指令

分支指令嚴重影響應用程序性能,大多數現代處理器利用指令預取緩存提高性能。在程序運行時,指令預取緩存被填充上順序的指令。

亂序引擎試圖儘可能快地執行指令。分支指令對亂序引擎有嚴重的破壞作用。

         編譯器創建彙編語言代碼時候,猜測if語句的then部分比else部分更可能被執行,試圖優化代碼的性能。

消除分支,例如在使用cmova前先試用cmp指令。有時候重複幾個額外的指令能夠消除跳轉。

         編寫可預測分支的代碼,把最可能採用的代碼安排在向前跳轉的順序執行語句中。

         展開循環,一般循環都可以通過向後分支規則預測,但是正確預測分支仍然有性能損失。簡單的循環也需要每次迭代時檢查計數器,還有必須計算的跳轉指令,根據循環內程序邏輯指令的數量,可能開銷也很大。對於比較小的循環,展開循環能夠解決這個問題,展開循環意味着手動地多次編寫每條指令的代碼,而不是使用循環返回相同的指令。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章