彙編函數閱讀筆記
- 2021 年 3 月 5 日
- 筆記
memset
原型
void memset(void* p_dst, char ch, int size)
這是memset
的函數原型,在C語言中使用這個函數時,需按這個原型傳參。
memset
的功能是:用size
個char
類型的數據填充初始記憶體地址是p_dst
的這片記憶體空間。
程式碼
global memset
memset:
push ebp
mov ebp, esp
push esi
push edi
push ecx
mov edi, [ebp + 8] ; Destination
mov edx, [ebp + 12] ; Char to be putted
mov ecx, [ebp + 16] ; Counter
.1:
cmp ecx, 0 ; 判斷計數器
jz .2 ; 計數器為零時跳出
mov byte [edi], dl ; ┓
inc edi ; ┛
dec ecx ; 計數器減一
jmp .1 ; 循環
.2:
pop ecx
pop edi
pop esi
mov esp, ebp
pop ebp
ret ; 函數結束,返回
解讀
函數模板
nasm
彙編寫函數的模板是:
; 函數名
funcName:
; 被修改的暫存器都要事先存儲到堆棧中,所以,ebp、eax、ebx、ecx都要入棧
push ebp
mov ebp, esp
push eax
push ebx
push ecx
; 調用funcName時,參數按照從右到左依次入棧
; ebp + 0 是 eip,ebp + 4 是esp
mov eax, [ebp + 16] ; 第三個參數
mov ebx, [ebp + 12] ; 第二個參數
mov ecx, [ebp + 8] ; 第一個參數
; some code
; some code
; 在函數末尾通過出棧還原被修改過的暫存器中的值,出棧順序和簽名的入棧順序相同
pop ecx
pop ebx
pop eax
pop ebp
; 函數末尾必須用這個指令結尾,出棧esp和eip。
ret
使用
要在其他文件中使用這個函數,需在本文件使用global memset
將此函數導出。
其他
.1:
cmp ecx, 0 ; 判斷計數器
jz .2 ; 計數器為零時跳出
mov byte [edi], dl ; ┓
inc edi ; ┛
dec ecx ; 計數器減一
jmp .1 ; 循環
dl
是第二個參數char ch
。char
是8個位元組,因此只需要使用暫存器dl
。
mov byte [edi], dl
把ch
填充到es:edi
記憶體空間。
.1:
;some code
;some code
jmp .1
用jmp
實現循環指令。能不用loop
指令就不用。loop
指令必須與ecx
配合使用,非常容易出錯。
out_byte
; 函數名稱是 out_byte
out_byte:
; esp 是 eip
mov edx, [esp + 4] ; port,第一個參數
mov al, [esp + 4 + 4] ; value,第二個參數
; 把al寫入dx埠
out dx, al
nop ; 一點延遲
nop
; 堆棧中的eip出棧
ret
in_byte
; 函數名稱是 in_byte
in_byte:
mov edx, [esp + 4] ; port,第一個參數
xor eax, eax ; 設置eax的值是0。異或運算,不相等結果是1,相等結果是0。
in al, dx ; 把dx埠的值寫入dl
nop ; 一點延遲
nop
ret
disp_str
程式碼
disp_str:
push ebp
mov ebp, esp
mov esi, [ebp + 8] ; pszInfo
mov edi, [disp_pos]
mov ah, 0Fh
.1:
lodsb
test al, al
jz .2
cmp al, 0Ah ; 是回車嗎?
jnz .3
push eax
mov eax, edi
mov bl, 160
div bl
and eax, 0FFh
inc eax
mov bl, 160
mul bl
mov edi, eax
pop eax
jmp .1
.3:
mov [gs:edi], ax
add edi, 2
jmp .1
.2:
mov [disp_pos], edi
pop ebp
ret
理解這個函數花了很多時間。原因是沒有及時聯想到讀寫顯示記憶體的坐標知識。
流程
流程是:
- 從參數
pszInfo
載入一個位元組數據到al
。 - 測試
al
是否為空。- 為空,函數結束。
- 非空
- 字元不是
回車
,列印字元,影片段偏移量自增2個位元組,然後跳轉到最外層流程1。 - 字元是回車,不列印這個字元,
- 計算出影片偏移量。計算方法是:
- 先計算出當前影片偏移量在第
N
行,計算公式是:影片偏移量/160
。 - 要列印的新字元所在行數應該是:
N+1
。 - 將新行數轉換成影片偏移量:
(N+1)*160
。
- 先計算出當前影片偏移量在第
- 跳轉到最外層流程1。
- 計算出影片偏移量。計算方法是:
- 字元不是
顯示記憶體坐標
[gs:(80*1+0)*2]
,把字元寫入第1行第1列。
[gs:(80*2+1)*2]
,把字元寫入第1行第2列。
指令
lodsb
lodsb
,把[ds:si]
處的數據讀入al
,si
自動自增1。
test
test al, al
jz .2
al
為空時,跳轉到.2
。
cmp
cmp al, 0Ah ; 是回車嗎?
jnz .3
al
的值不是0Ah
時,跳轉到.3
。0Ah
是回車鍵的ASCII碼。
div
mov eax, edi
mov bl, 160
div bl
and eax, 0FFh
div
是除法。被除數在eax
中,除數在bl
中,商在al
中,餘數在ah
中。
and eax, 0FFh
獲取al
中的值。
disp_color_str
disp_color_str:
push ebp
mov ebp, esp
mov esi, [ebp + 8] ; pszInfo
mov edi, [disp_pos]
mov ah, [ebp + 12] ; color
.1:
lodsb
test al, al
jz .2
cmp al, 0Ah ; 是回車嗎?
jnz .3
push eax
mov eax, edi
mov bl, 160
div bl
and eax, 0FFh
inc eax
mov bl, 160
mul bl
mov edi, eax
pop eax
jmp .1
.3:
mov [gs:edi], ax
add edi, 2
jmp .1
.2:
mov [disp_pos], edi
pop ebp
ret
只在disp_str
的基礎上增加了一句mov ah, [ebp + 12] ; color
,設置列印字元串的顏色,其他部分與disp_str
完全一致。
restart
restart:
mov esp, [p_proc_ready]
lldt [esp + P_LDT_SEL]
lea eax, [esp + P_STACKTOP]
mov dword [tss + TSS3_S_SP0], eax
pop gs
pop fs
pop es
pop ds
popad
add esp, 4
iretd
這是構造好進程後,啟動第一個進程使用的函數。
語法很好懂,難點在業務邏輯。
指令
lldt
lldt [esp + P_LDT_SEL]
,載入LDT。[esp + P_LDT_SEL]
是LDT的選擇子。
lea
假設,si = 1000h,ds = 50000h,(51000h)=1234h,那麼
lea ax, [ds:si]
執行後,ax的值是1000h
。mov ax, [d:si]
執行後,ax的值是1234h
。
lea eax, [esp + P_STACKTOP]
執行後,eax
的值是一個偏移量,是記憶體地址,而不是記憶體esp + P_STACKTOP
中的值。
popad
popad
依次出棧EDI,ESI,EBP,ESP,EBX,EDX,ECX,EAX
。
iretd
暫時沒有找到權威資料。
業務邏輯
[p_proc_ready]
指向進程表的初始位置。j- 初始位置正好是存儲暫存器的結構regs。
P_LDT_SEL
是regs的長度。[esp + P_LDT_SEL]
跳過regs,指向進程表的第二個成員結構,是LDT的選擇子。- 其實
[esp + P_LDT_SEL]
和[esp + P_STACKTOP]
指向同一個記憶體單元。總之,這個時候,指向regs的棧頂。 mov dword [tss + TSS3_S_SP0], eax
,讓TSS
的sp0
指向regs的棧頂。- 接下來,執行pop操作時,CPU挑選的是0特權級的
ss0
和sp0
,所以,出棧的棧是進程表中的regs。