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:
; 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:
mov edx, [esp + 4] ; port,第一个参数
xor eax, eax ; 设置eax的值是0。异或运算,不相等结果是1,相等结果是0。
in al, dx ; 把dx端口的值写入dl
nop ; 一点延迟
nop
ret
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
。[gs:(80*1+0)*2]
,把字符写入第1行第1列。
[gs:(80*2+1)*2]
,把字符写入第1行第2列。
lodsb
,把[ds:si]
处的数据读入al
,si
自动自增1。
test al, al
jz .2
al
为空时,跳转到.2
。
cmp al, 0Ah ; 是回车吗?
jnz .3
al
的值不是0Ah
时,跳转到.3
。0Ah
是回车键的ASCII码。
mov eax, edi
mov bl, 160
div bl
and eax, 0FFh
div
是除法。被除数在eax
中,除数在bl
中,商在al
中,余数在ah
中。
and eax, 0FFh
获取al
中的值。
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:
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 [esp + P_LDT_SEL]
,加载LDT。[esp + P_LDT_SEL]
是LDT的选择子。
假设,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
依次出栈EDI,ESI,EBP,ESP,EBX,EDX,ECX,EAX
。
暂时没有找到权威资料。
[p_proc_ready]
指向进程表的初始位置。jP_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的栈顶。ss0
和sp0
,所以,出栈的栈是进程表中的regs。