彙編函數閱讀筆記

memset

原型

void memset(void* p_dst, char ch, int size)

這是memset的函數原型,在C語言中使用這個函數時,需按這個原型傳參。

memset的功能是:用sizechar類型的數據填充初始記憶體地址是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 chchar是8個位元組,因此只需要使用暫存器dl

mov byte [edi], dlch填充到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

理解這個函數花了很多時間。原因是沒有及時聯想到讀寫顯示記憶體的坐標知識。

流程

流程是:

  1. 從參數pszInfo載入一個位元組數據到al
  2. 測試al是否為空。
    1. 為空,函數結束。
    2. 非空
      1. 字元不是回車,列印字元,影片段偏移量自增2個位元組,然後跳轉到最外層流程1。
      2. 字元是回車,不列印這個字元,
        1. 計算出影片偏移量。計算方法是:
          1. 先計算出當前影片偏移量在第N行,計算公式是:影片偏移量/160
          2. 要列印的新字元所在行數應該是:N+1
          3. 將新行數轉換成影片偏移量:(N+1)*160
        2. 跳轉到最外層流程1。

顯示記憶體坐標

[gs:(80*1+0)*2],把字元寫入第1行第1列。

[gs:(80*2+1)*2],把字元寫入第1行第2列。

指令

lodsb

lodsb,把[ds:si]處的數據讀入alsi自動自增1。

test

test	al, al
jz	.2

al為空時,跳轉到.2

cmp

cmp	al, 0Ah	; 是回車嗎?
jnz	.3

al的值不是0Ah時,跳轉到.30Ah是回車鍵的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,那麼

  1. lea ax, [ds:si]執行後,ax的值是1000h
  2. 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

暫時沒有找到權威資料。

業務邏輯

  1. [p_proc_ready]指向進程表的初始位置。j
  2. 初始位置正好是存儲暫存器的結構regs。
  3. P_LDT_SEL是regs的長度。[esp + P_LDT_SEL] 跳過regs,指向進程表的第二個成員結構,是LDT的選擇子。
  4. 其實[esp + P_LDT_SEL] [esp + P_STACKTOP]指向同一個記憶體單元。總之,這個時候,指向regs的棧頂。
  5. mov dword [tss + TSS3_S_SP0], eax,讓TSSsp0指向regs的棧頂。
  6. 接下來,執行pop操作時,CPU挑選的是0特權級的ss0sp0,所以,出棧的棧是進程表中的regs。