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
使用
要在其他文件中使用这个函数,需在本文件使用globalmemset
将此函数导出。
其他
.1:
cmp ecx, 0 ; 判断计数器
jz .2 ; 计数器为零时跳出
mov byte [edi], dl ; ┓
inc edi ; ┛
dec ecx ; 计数器减一
jmp .1 ; 循环
dl
是第二个参数char ch
。char
是8个字节,因此只需要使用寄存器dl
。
movbyte [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
中。
andeax, 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
的基础上增加了一句movah, [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
。
leaeax, [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的栈顶。 movdword [tss + TSS3_S_SP0], eax
,让TSS
的sp0
指向regs的栈顶。- 接下来,执行pop操作时,CPU挑选的是0特权级的
ss0
和sp0
,所以,出栈的栈是进程表中的regs。