條件跳轉

在此之前我們使用的彙編代碼示例都是從第一條指令開始,直到最後最後一條指令程序退出。但實際上和高級語言類似,彙編代碼也提供指令來改變程序處理數據方式。

正常情況下,程序要執行要執行的下一條指令是在指令指針寄存器中,指令指針確定程序中哪條指令是應該執行的下一條指令。

當指令指針在程序指令中移動時,EIP寄存器會遞增。指令長度可能是多個字節,所以指向下一條指令不僅僅是每次是指令指針遞增一。

指令指針寄存器(EIP)跟蹤要執行程序的下一條指令代碼,應用程序不能修改指令指針本身,不能使用指定的內存地址放在EIP中,相反必須使用能夠改變指令指針的指令來改變預存取緩存的下一條指令,這些指令稱為分支指令。分支指令可以改變EIP寄存器的值,要麼是無條件改變,要麼是按照條件改變。

當程序遇到跳轉、調用、中斷時,指令指針自動跳轉到另一個位置。

  • 跳轉指令

跳轉指令使用單一指令碼:

jmp location
其中location是要跳轉到的內存地址。在彙編語言中這個位置是程序代碼中的標籤, 類似於C語言中的goto語句。遇到該指令時,指令指針改變為該標籤後面的指令碼的內存地址。下面示例演示跳轉指令操作:
# jmp.s
.section .text
.globl _start
_start:
    nop
    movl $1, %eax
    jmp gotohere
    movl $10, %ebx
    int $0x80
gotohere:
    movl $20, %ebx
    int $0x80

編譯執行,查看程序返回結果:

$ make
as -o jmp.o jmp.s --gstabs
ld -o jmp jmp.o
$ ./jmp
$ echo $?
20

程序簡單調用系統調用exit,通過查看程序執行返回碼就可以確定跳轉發生了。我們也可以在調試程序中單步運行查看運行的每行代碼來確定跳轉的發生。如下:

(gdb) b *_start
Breakpoint 1 at 0x8048054: file jmp.s, line 5.
(gdb) r
Starting program: /home/allen/as/4_jmp/jmp

Breakpoint 1, _start () at jmp.s:5
5    nop
(gdb) s
6    movl $1, %eax
(gdb) s
7    jmp gotohere
(gdb) s
11    movl $20, %ebx
(gdb) s
12    int $0x80
(gdb) s

Program exited with code 024.
(gdb)

重新編譯程序,去掉調試信息,使用objdump程序反彙編可執行程序,可以瞭解指令碼在內存中是如何安排的:

$ as -o jmp.o jmp.s
$ ld -o jmp jmp.o
$ objdump -D jmp
jmp:     file format elf32-i386
Disassembly of section .text:
08048054 <_start>:
8048054: 90                    nop
8048055: b8 01 00 00 00        mov    $0x1,%eax
804805a: eb 07                 jmp    8048063 <gotohere>
804805c: bb 0a 00 00 00        mov    $0xa,%ebx
8048061: cd 80                 int    $0x80
08048063 <gotohere>:
8048063: bb 14 00 00 00        mov    $0x14,%ebx
8048068: cd 80                 int    $0x80

現在可以通過程序make編譯程序,在調試程序中對照上面反彙編中指令碼內存位置查看eip寄存器的值。

Breakpoint 1, _start () at jmp.s:5
5    nop
(gdb) n
6    movl $1, %eax
(gdb) n
7    jmp gotohere
(gdb) print $eip
$1 = (void (*)()) 0x804805a <_start+6>
(gdb) n
11    movl $20, %ebx
(gdb) print $eip
$2 = (void (*)()) 0x8048063 <gotohere>
(gdb)

可以看到,輸出eip地址0x8048063就是gotohere標籤指向的內存位置。

  • 調用指令

調用指令類似跳轉指令,但是它保存發生跳轉的位置,在必要時可以返回這個位置。在彙編語言中,實現函數就使用調用指令。類似C語言,彙編語言函數也是分割功能模塊,避免多次編寫相同代碼。調用指令用法:

call addr

addr為操作數引用程序中的標籤,其被轉換為函數中第一條指令的內存地址。函數返回代碼原始部分使用助記符ret。執行call指令時,指令把EIP寄存器的值放到堆棧中,然後修改EIP寄存器以指向被調用的函數地址。當被調用的函數完成後,它從堆棧獲得過去的EIP寄存器值,並且把控制權返回給原始程序,因為在函數中可能對堆棧進行操作,所以EBP經常用作堆棧的基指針,因此在函數的開始通常也把ESP寄存器複製到EBP寄存器中。我們可以因此給出一個彙編語言函數的模板:

func_lable:
    push1 %ebp
    movl %esp, %ebp
    <function code here>
    movl %ebp, %esp
    popl %ebp
    ret

在保存EBP寄存器之後就可以使用它作為堆棧的基指針,在函數中進行對堆棧的所有訪問。在函數返回之前,ESP寄存器必須被恢復為指向發出調用的內存位置。 下面演示一個簡單的call示例:

#call.s
.section .data
msg:
    .asciz "this is as call test!\n"
    len=.-msg
.section .text
.globl _start
_start:
    nop
    call output_func
    movl $0, %ebx
    movl $1, %eax
    int $0x80

output_func:
    pushl %ebp
    movl %esp, %ebp

    #<function code here>
    movl $len, %edx
    movl $msg, %ecx
    movl $1, %ebx
    movl $4, %eax
    int $0x80

    movl %ebp, %esp
    popl %ebp
    ret

程序調用函數output_func輸出一串字符串。make並執行結果如下:

$ make
as -o call.o call.s --gstabs
ld -o call call.o
$ ./call this is as call test!
$
  • 中斷 中斷也可以更改當前指令指針。中斷分軟中斷和硬中斷,當一個程序被中斷時,指針指針被轉移到被調用的程序,並且從被調用的程序內繼續執行,被調用的程序完成時,它可以把控制返回給發出調用的程序。在之前幾節給出的示例中,已經使用過中斷。簡單的使用0x80值的INT指令把控制轉移給linux系統調用程序,在中斷髮生時,按照eax寄存器的值執行子函數,有關中斷詳細信息在後面會講到。

  • 條件跳轉 條件跳轉按照EFLAGS中的值來判斷是否該跳轉。每個條件跳轉指令都檢查特定的標誌位以便確定是否符合跳轉的條件。EFLAGS寄存器中有很多位,和條件跳轉有關的有5位:0位(進位標誌CF)、11位(溢出標誌OF)、2位(奇偶校驗標誌PF)、7位(符號標誌SF)、第6位(零標誌ZF)。結合這幾個不同的標誌位可以執行多種跳轉組合。條件跳轉指令格式如下:

jxx addr

其中xx是1個到3個字符的條件代碼,addr是程序要跳轉到的位置。下面是所有可用的條件跳轉指令。

a 大於時跳轉
ae 大於等於
b 小於
be 小於等於
c 進位
cxz 如果CX寄存器為0
ecxz 如果ECS寄存器為0
e 相等
na 不大於
nae 不大於或者等於
nb 不小於
nbe 不小於或等於
nc 無進位
ne 不等於
g 大於(有符號)
ge 大於等於(有符號)
l 小於(有符號)
le 小於等於(有符號)
ng 不大於(有符號)
nge 不大於等於(有符號)
nl 不小於
nle 不小於等於
no 不溢出
np 不奇偶校驗
ns 無符號
nz 非零
o 溢出
p 奇偶校驗
pe 如果偶校驗
po 如果奇校驗
s 如果帶符號
z 如果為零

EFLAGS寄存器可以通過比較指令比較兩個值來設置,比較指令CMP格式如下:

cmp operand1, operand2

指令將第二個操作數和第一個操作數進行比較,它對兩個操作數執行減法操作(operand2-operand1),然後設置EFALGS寄存器。如下示例:

#cmp.s
.section .text
.globl _start
_start:
    nop
    movl $11, %eax
    movl $24, %ebx
    cmp %eax, %ebx
    jae greater
    movl $1, %eax
    int $0x80

greater:
    movl $11, %ebx
    movl $1, %eax
    int $0x80

make,運行結果如下:

$ make
as -o cmp.o cmp.s --gstabs
ld -o cmp cmp.o
$ ./cmp
$ echo $?
11

通過查看程序返回輸出結果說明發生了條件跳轉,ebx寄存器的值大於大小寄存器中值,所以代碼跳轉到greater標籤處執行,也可以在調試器中單步運行查看代碼執行順序。


书籍推荐