第23部分- Linux x86 64位彙編 字符串存儲加載
除了字符串從一個內存位置傳送到另一個內存位置外,還有用於把內存中的字符串值加載到寄存器以及傳會至內存位置中的指令。
lods
lods指令把內存中的字符串值傳送到EAX寄存器中。
有多種格式:lodsb, lodsw, lodsl,lodsq
使用ESI寄存器作爲隱藏的源操作數。
stos
STOS指令可以把EAX寄存器中的數據存放到一個內存位置中。
有STOSB,STOSW,STOSL,STOSQ。
使用EDI寄存器作爲隱含的目標操作數。
加載示例
-
.extern printf ;//調用外部的printf函數
-
.section .data
-
space:
-
.ascii "h"
-
oa:
-
.byte 0x0a
-
end:
-
.byte 0
-
.section .bss
-
.lcomm stobuffer, 256
-
.section .text
-
.globl _start
-
_start:
-
nop
-
leal space, %esi
-
leal stobuffer, %edi
-
movl $255, %ecx
-
cld
-
lodsb
-
rep stosb
-
-
leal oa, %esi
-
lodsb
-
stosb
-
leal end, %esi
-
lodsb
-
lodsb
-
-
movq $stobuffer,%rdi
-
call printf
-
-
movq $60,%rax
-
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
比較示例
-
.extern printf ;//調用外部的printf函數
-
.section .data
-
string1:
-
.ascii "test1"
-
length1:
-
.int 5
-
string2:
-
.ascii "test1"
-
length2:
-
.int 5
-
big:
-
.ascii "big"
-
.byte 0x0a,0x00
-
-
small:
-
.ascii "small"
-
.byte 0x0a,0x00
-
-
equ:
-
.ascii "equal"
-
.byte 0x0a,0x00
-
-
.section .text
-
.globl _start
-
_start:
-
nop
-
lea string1, %esi
-
lea string2, %edi
-
movl length1, %ecx
-
movl length2, %eax
-
cmpl %eax, %ecx;//比較兩個字符串長度,短的加載到寄存器,供REPE使用
-
ja longer;//如果字符串2長跳轉到longer,不用交換
-
xchg %ecx, %eax;//否則,進行交換
-
longer:
-
cld
-
repe cmpsb
-
je equal;//最小長度下都相等,需要比較字符串長度。
-
jg greater
-
-
less:
-
movq $small,%rdi
-
call printf
-
movq $60,%rax
-
syscall
-
-
greater:
-
movq $big,%rdi
-
call printf
-
movq $60,%rax
-
syscall
-
-
equal:;// 比較字符串長度。
-
movl length1, %ecx
-
movl length2, %eax
-
cmpl %ecx, %eax
-
jg greater
-
jl less
-
movq $equ,%rdi
-
call printf
-
-
movq $60,%rax
-
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常常用於找到搜索字符時停止掃描。
掃描字符串示例
-
.extern printf ;//調用外部的printf函數
-
.section .data
-
string1:
-
.ascii "This is a test - a long text string to scan."
-
length:
-
.int 44
-
string2:
-
.ascii "-"
-
noresult:
-
.ascii "No result."
-
.byte 0x0a,0x0
-
yesresult:
-
.ascii "Yes have result."
-
.byte 0x0a,0x0
-
.section .text
-
.globl _start
-
_start:
-
nop
-
leal string1, %edi
-
leal string2, %esi
-
movl length, %ecx
-
lodsb;//加載第一個-到al寄存器
-
cld
-
repne scasb;//掃描字符串string1,知道找到-字符。
-
jne notfound
-
movq $yesresult,%rdi
-
call printf
-
-
movq $60,%rax
-
syscall
-
notfound:
-
movq $noresult,%rdi
-
call printf
-
-
movq $60,%rax
-
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寄存器中的字符序列。
-
.extern printf ;//調用外部的printf函數
-
.section .data
-
string1:
-
.ascii "This is a test - a long text string to scan."
-
length:
-
.int 11
-
string2:
-
.ascii "test"
-
noresult:
-
.ascii "No result."
-
.byte 0x0a,0x0
-
yesresult:
-
.ascii "Yes have result."
-
.byte 0x0a,0x0
-
-
.section .text
-
.globl _start
-
_start:
-
nop
-
leal string1, %edi
-
leal string2, %esi
-
movl length, %ecx
-
lodsl;//加載test到eax寄存器
-
cld
-
repne scasl;//進行比較知道找到位置
-
jne notfound
-
subw length, %cx
-
neg %cx
-
-
movq $yesresult,%rdi
-
call printf
-
-
movq $60,%rax
-
syscall
-
-
notfound:
-
movq $noresult,%rdi
-
call printf
-
-
movq $60,%rax
-
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指令有一個功能是確定零結尾(也稱爲空結尾)的字符串長度。
搜索零的位置,找到後就可以會到總共搜索了多少個字符,也就是字符的長度。
-
.extern printf ;//調用外部的printf函數
-
.section .data
-
string1:
-
.asciz "Testing, one, two, three, testing.\n"
-
-
noresult:
-
.ascii "No Result."
-
.byte 0x0a,0x0
-
yesresult:
-
.ascii "Yes have result : %d."
-
.byte 0x0a,0x0
-
-
.section .text
-
.globl _start
-
_start:
-
nop
-
leal string1, %edi
-
movl $0xffff, %ecx;//假設字符串長65535
-
movb $0, %al
-
cld
-
repne scasb
-
jne notfound
-
subw $0xffff, %cx;//減去初始值,變成了負值
-
neg %cx;//取反就是正數了
-
dec %cx;//去掉最後一個空符號,就是字符串長度了。
-
-
movq $yesresult,%rdi
-
movq %rcx,%rsi
-
call printf
-
-
movq $60,%rax
-
syscall
-
-
notfound:
-
movq $noresult,%rdi
-
call printf
-
-
movq $60,%rax
-
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 中,無法操作位置計數器,必須使用標籤計算下一個存儲位置(數據、指令等等)。
-
.section .data
-
.equ SYS_WRITE,1
-
.equ STD_OUT,1
-
.equ SYS_EXIT,60
-
.equ EXIT_CODE,0
-
.equ NEW_LINE,0xa
-
INPUT: .ascii " Hello world!\n"
-
.section .bss;;//定義buffer緩存
-
.lcomm OUTPUT,12
-
.global _start
-
.section .text
-
_start:
-
movq $INPUT ,%rsi;//將字符串地址賦值爲rsi
-
xor %rcx, %rcx;//清零
-
cld;//清空df標誌位0
-
-
jmp calculateStrLength;//調用函數計算字符串長度
-
reversePre:
-
xor %rax, %rax;//清空rax
-
xor %rdi, %rdi;//清空rdi
-
jmp reverseStr;//跳轉到reverseStr函數進行翻轉
-
-
calculateStrLength:;//計算INPUT字符串的長度,並保存在rcx中。
-
cmpb $0 ,(%rsi);//與0對比,字符串最後一個’\0’字符串結束標誌
-
je exitFromRoutine;//函數結束
-
lodsb;//從rds:rsi處加載一個字節到al,並增加rsi
-
pushq %rax;/將剛獲取到的al進行壓棧,不過這會破壞了原本返回地址的位置。返回使用通過push rdi來解決。
-
inc %rcx;//增加計數器
-
jmp calculateStrLength;//循環
-
exitFromRoutine:;//返回到_start主線。
-
pushq %rdi;//壓棧return地址到棧中
-
-
jmp reversePre;
-
reverseStr:
-
cmpq $0,%rcx;//對比0,rcx已保存的是字符串的長度
-
je printResult;//相等則跳轉到printResult
-
popq %rax;//從棧中彈出一個字符給rax
-
movq %rax,OUTPUT(%rdi);//寫到[OUTPUT+rdi]對應的地址
-
dec %rcx;//寫一個少一個
-
inc %rdi;//寫一個地址加一個
-
jmp reverseStr;//返回循環
-
printResult:
-
mov %rdi, %rdx;//輸出的長度賦值到rdx
-
mov $SYS_WRITE,%rax;//調用號
-
mov $STD_OUT, %rdi;//輸出句柄
-
mov $OUTPUT, %rsi;//輸出字符串地址
-
syscall;//調用sys_write輸出OUTPUT保存的反向字符串。
-
jmp printNewLine;//調用函數輸出換行符
-
-
printNewLine:
-
mov $SYS_WRITE, %rax;//調用號
-
mov $STD_OUT, %rdi;//輸出句柄
-
mov $NEW_LINE, %rsi;//輸出字符串地址
-
mov $1,%rdx;//輸出的長度賦值到rdx
-
syscall
-
jmp exit
-
-
exit:
-
mov $SYS_EXIT ,%rax
-
mov $EXIT_CODE ,%rdi
-
syscall
編譯:
as -g -o reverse_att.o reverse_att.s
ld -o reverse_att reverse_att.o
Intel彙編實現
-
section .data
-
SYS_WRITE equ 1
-
STD_OUT equ 1
-
SYS_EXIT equ 60
-
EXIT_CODE equ 0
-
-
NEW_LINE db 0xa
-
INPUT db "Hello world!"
-
section .bss;;//定義buffer緩存
-
OUTPUT resb 12
-
global _start
-
section .text
-
_start:
-
mov rsi, INPUT;//將字符串地址賦值爲rsi
-
xor rcx, rcx;//清零
-
cld;//清空df標誌位0
-
mov rdi, $ + 15;//預處理rdi寄存器,在calculateStrLength函數中會用到。$是表示當前指令的地址。$$是當前段的地址。通過$+15可以計算得到命令xor rax,rax這條指令的地址(可以通過objdump -D命令來數出來),將該地址賦值給rdi進行了保存。在calculateStrLength函數中可以直接獲取rdi地址來返回到xor rax,rax命令。
-
call calculateStrLength;//調用函數計算字符串長度
-
xor rax, rax;//清空rax
-
xor rdi, rdi;//清空rdi
-
jmp reverseStr;//跳轉到reverseStr函數進行翻轉
-
-
calculateStrLength:;//計算INPUT字符串的長度,並保存在rcx中。
-
cmp byte [rsi], 0;//與0對比,字符串最後一個’\0’字符串結束標誌
-
je exitFromRoutine;//函數結束
-
lodsb;//從rds:rsi處加載一個字節到al,並增加rsi
-
push rax;/將剛獲取到的al進行壓棧,不過這會破壞了原本返回地址的位置。返回使用通過push rdi來解決。
-
inc rcx;//增加計數器
-
jmp calculateStrLength;//循環
-
exitFromRoutine:;//返回到_start主線。
-
push rdi;//壓棧return地址到棧中
-
ret
-
reverseStr:
-
cmp rcx, 0;//對比0,rcx已保存的是字符串的長度
-
je printResult;//相等則跳轉到printResult
-
pop rax;//從棧中彈出一個字符給rax
-
mov [OUTPUT + rdi], rax;寫到[OUTPUT+rdi]對應的地址
-
dec rcx;//寫一個少一個
-
inc rdi;//寫一個地址加一個
-
jmp reverseStr;//返回循環
-
-
printResult:
-
mov rdx, rdi;//輸出的長度賦值到rdx
-
mov rax, SYS_WRITE;//調用號
-
mov rdi, STD_OUT;//輸出句柄
-
mov rsi, OUTPUT;//輸出字符串地址
-
syscall;//調用sys_write輸出OUTPUT保存的反向字符串。
-
jmp printNewLine;//調用函數輸出換行符
-
-
printNewLine:
-
mov rax, SYS_WRITE;//調用號
-
mov rdi, STD_OUT;//輸出句柄
-
mov rsi, NEW_LINE;//輸出字符串地址
-
mov rdx, 1;//輸出的長度賦值到rdx
-
syscall
-
jmp exit
-
-
exit:
-
mov rax, SYS_EXIT
-
mov rdi, EXIT_CODE
-
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 指令後面跟着宏名稱,後面可以有參數,也可以沒有參數。宏參數是按名稱指定的。
-
.macro write_string p1,p2
-
movq $1,%rax;//調用號sys_write
-
movq $1,%rdi;//輸出到stdout
-
movq \p1,%rsi;//字符串地址
-
movq \p2,%rdx;//字符串長度
-
syscall
-
.endm
-
-
.section .data
-
msg1: .ascii "Hello,programmers!\n"
-
len1: .quad len1-msg1
-
-
msg2: .asciz "Welcome to the world of,\n"
-
len2: .quad len2-msg2
-
-
msg3: .asciz "Linux assembly programming!\n"
-
len3: .quad len3-msg3
-
-
.section .text
-
.global _start
-
_start:
-
write_string $msg1,len1;//第一個參數是字符串地址,第二個參數是字符串長度不是其地址。
-
write_string $msg2,len2
-
write_string $msg3,len3
-
-
movq $1,%rax ;//系統調用號(sys_exit)
-
int $0x80 ;//調用內核
編譯:
as -g -o macro_att.o macro_att.s
ld -o macro_att macro_att.o
Gas數據結構
數據結構如下:
-
.extern printf ;//調用外部的printf函數
-
.section .data
-
fmt: .ascii "name = %s, high = %ld \n"
-
-
.set name_size,20
-
.set high_size,4
-
-
people:
-
name: .space name_size
-
high: .space high_size
-
-
xiaoming:
-
xm_name: .ascii "xiaoming"
-
xm_null: .byte 0
-
xm_high: .quad 0xaf
-
-
.section .text
-
.global _start
-
_start:
-
movq $fmt, %rdi
-
movq $xm_name,%rsi
-
movq xm_high, %rdx
-
movq $0, %rax
-
call printf
-
-
movq $60, %rax
-
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結尾。
|
宏名稱後面是一個數字,是宏需要的宏參數數量。在 NASM 中,宏參數是從 1 開始連續編號的。宏的第一個參數是 %1,第二個是 %2,第三個是 %3,以此類推。
例子代碼如下:
-
; A macro with two parameters
-
; Implements the write system call
-
%macro write_string 2
-
mov rax, 4
-
mov rbx, 1
-
mov rcx, %1
-
mov rdx, %2
-
int 80h
-
%endmacro
-
-
section .text
-
global _start ;must be declared for using gcc
-
-
_start: ;tell linker entry point
-
write_string msg1, len1
-
write_string msg2, len2
-
write_string msg3, len3
-
-
mov rax,1 ;system call number (sys_exit)
-
int 0x80 ;call kernel
-
-
section .data
-
msg1 db 'Hello, programmers!',0xA,0xD
-
len1 equ $ - msg1
-
-
msg2 db 'Welcome to the world of,', 0xA,0xD
-
len2 equ $- msg2
-
-
msg3 db 'Linux assembly programming! ',0xA,0xD
-
len3 equ $- msg3
nasm -g -f elf64 -o macro.o macro.asm
ld -o macro macro.o
Nasm結構使用
在C語言中可以使用struct聲明結構體,而在nasm彙編中,也可以使用結構體,通過使用僞指令來聲明結構體。
使用數據結構例子如下,還實現了外部函數的調用:
-
.extern printf ;//調用外部的printf函數
-
.section .data
-
fmt: .ascii "name = %s, high = %ld \n"
-
-
.set name_size,20
-
.set high_size,4
-
-
people:
-
name: .space name_size
-
high: .space high_size
-
-
xiaoming:
-
xm_name: .ascii "xiaoming"
-
xm_null: .byte 0
-
xm_high: .quad 0xaf
-
-
.section .text
-
.global _start
-
_start:
-
movq $fmt, %rdi
-
movq $xm_name,%rsi
-
movq xm_high, %rdx
-
movq $0, %rax
-
call printf
-
-
movq $60, %rax
-
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指令鏈接在一起。
示例
代碼如下:
-
.section .data
-
data1:
-
.quad 11111111,7252051615;//16字節整數
-
data2:
-
.quad 22222222,5732348928;//16字節整數
-
output:
-
.asciz "The result is %qd\n"
-
.section .text
-
.globl _start
-
_start:
-
movq data1, %rbx;//保存data1的前8個字節到ebx
-
movq data1+8, %rax;//保存data1的後8個字節到ebx
-
movq data2, %rdx;//保存data2的前8個字節到ebx
-
movq data2+8, %rcx;//保存data2的後8個字節到ebx
-
addq %rbx, %rdx;//前8個字節相加
-
adcq %rcx, %rax;//後8個字節相加
-
adcq %rdx, %rax;//全部相加
-
movq $output,%rdi
-
movq %rax ,%rsi
-
call printf
-
movq $60,%rax
-
movq $0,%rdi
-
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是一個立即值。
乘法實例
-
.section .data
-
output:
-
.asciz "The result is %d, %ld\n"
-
value1:
-
.int 10
-
value2:
-
.int -35
-
value3:
-
.int 400
-
.section .text
-
.globl _start
-
_start:
-
nop
-
movl value1, %ebx;//移動value1到ebx
-
movl value2, %ecx;//移動value2到ecx
-
imull %ebx, %ecx;//將ebx和ecx相乘結果保存到ecx
-
movl value3, %edx;//移動value3到edx
-
imull $2, %edx, %eax;//繼續相差
-
movq $output,%rdi
-
movl %ecx ,%esi;//商
-
movl %eax ,%edx;//餘數
-
call printf
-
movl $1, %eax
-
movl $0, %ebx
-
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只有一個參數值。
除法操作如下:
除法實例
-
.section .data
-
dividend:
-
.quad 8335
-
divisor:
-
.int 25
-
quotient:
-
.int 0
-
remainder:
-
.int 0
-
output:
-
.asciz "<93>The quotient is %d, and the remainder is %d\n<94>"
-
.section .text
-
.globl _start
-
_start:
-
nop
-
movl dividend, %eax
-
movl dividend+4, %edx
-
divw divisor ;//除數
-
movl %eax, quotient
-
movl %edx, remainder
-
-
movq $output,%rdi
-
movl quotient,%esi;//商
-
movl remainder,%edx;//餘數
-
-
call printf
-
movl $1, %eax
-
movl $0, %ebx
-
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位彙編 數據傳輸
無符號條件傳送指令
無符號條件傳送指令依靠進位、零和奇偶校驗標誌來確定兩個操作數之間的區別。
帶符號條件傳送指令如下:
帶符號條件傳送指令使用符號和溢出標誌表示操作數之間比較的狀態。
示例
實例如下,查找數組中一系列整數中最大的一個。
-
.section .data
-
output:
-
.asciz "The largest value is %d\n";//定義字符串
-
values:
-
.int 105, 235, 61, 315, 134, 221, 53, 145, 117, 5;//定義整型
-
.section .text
-
.globl _start
-
_start:
-
nop
-
movl values, %ebx;//ebx保存最大的整數,第一個是105
-
movl $1, %edi;//移動計數
-
loop:
-
movl values(, %edi, 4), %eax;//逐個加載到eax寄存器。
-
cmp %ebx, %eax;//和ebx比較
-
cmova %eax, %ebx;//eax大於ebx,則將eax移動到ebx。
-
inc %edi;//增加edi
-
cmp $10, %edi;//是否已經對比了10個。
-
jne loop
-
movq $output,%rdi
-
movq %rbx,%rsi
-
call printf
-
movq $60,%rax
-
movq $0,%rdi
-
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是外層循環。
如下:
-
.section .data
-
-
values:
-
.int 105, 235, 61, 315, 134, 221, 53, 145, 117, 5
-
.int 0
-
.section .text
-
.globl _start
-
_start:
-
movl $values, %esi;//數組地址存放於esi
-
movl $9, %ecx;//外層循環次數
-
movl $9, %ebx;//內層循環次數
-
loop:
-
movl (%esi), %eax;//第一個值或當前數值移動給eax
-
cmp %eax, 4(%esi) ;//第一個值或當前數值和下一個值進行比較
-
jge skip;//如果大於等於第二個值則跳轉到skip,否則用下面兩條命令進行交換,將小的值往後冒泡
-
xchg %eax, 4(%esi)
-
movl %eax, (%esi)
-
skip:
-
add $4, %esi;//繼續指向下一個值
-
dec %ebx;//內存循環遞減
-
jnz loop
-
dec %ecx;//內存循環結束,外層循環遞減
-
jz end;//外層循環結束
-
movl $values, %esi;//內存循環重新開始前重置esi,並設置ecx,ebx。
-
movl %ecx, %ebx
-
jmp loop
-
end:
-
movl $1, %eax
-
movl $0, %ebx
-
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指令。有時候重複幾個額外的指令能夠消除跳轉。
編寫可預測分支的代碼,把最可能採用的代碼安排在向前跳轉的順序執行語句中。
展開循環,一般循環都可以通過向後分支規則預測,但是正確預測分支仍然有性能損失。簡單的循環也需要每次迭代時檢查計數器,還有必須計算的跳轉指令,根據循環內程序邏輯指令的數量,可能開銷也很大。對於比較小的循環,展開循環能夠解決這個問題,展開循環意味着手動地多次編寫每條指令的代碼,而不是使用循環返回相同的指令。