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。