汇编基础

汇编基础

MS-Windows编程

Win32 控制台编程

链接库用了这么久了,汇编学了这么多了,HelloWorld还不会写。
本章介绍如何用32位Microsoft Windows API(application programming interface)进行控制台窗口编程。
虽然不建议用汇编语言进行扩展图形应用编程,但不影响我们从底层理解高级语言的GUI编程

背景知识

  • 一个Windows应用程序开始的时候,要吗创建一个控制台窗口,要吗创建一个图形化窗口。这里我们一直使用SUBSYSTEAM:CONSOLE,告诉链接器创建一个基于控制台的应用程序

  • 控制台程序的外观和操作就像MS-DOS窗口一样,控制台有一个输入缓冲区以及一个或多个屏幕缓冲区:

    • 输入缓存区(input buffer)包含一组输入记录(input records),其中每个记录都是一个输入事件的数据。输入事件的例子包括键盘输入,鼠标点击,以及用户调整控制台窗口大小
    • 屏幕缓冲区(screen buffer)是字符与颜色数据的二维数组,他会影响控制台窗口文本的外观
  • 调用Win32 API函数时会使用两类字符集:8位的ASCII/ANSI字符集和16位的Unicode字符集

  • 控制台存在访问级别,用于在简单控制和完全控制直接进行权衡

Win32 API

ps:详细的内容请参考官网

以例子来简单了解如何使用

获取用户输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
INCLUDE Irvine32.inc
BufSize = 80
.data
buffer BYTE BufSize DUP(?), 0, 0
stdInHandle HANDLE ?
bytesRead DWORD ?
.code
main PROC
;获取标准输入句柄
INVOKE GetStdHandle ,STD_INPUT_HANDLE
mov stdInHandle, eax
;等待用户输入
INVOKE ReadConsole, stdInHandle, ADDR buffer,
BufSize, ADDR bytesRead, 0
;显示缓冲区
mov esi, OFFSET buffer
mov ecx, bytesRead
mov ebx, TYPE buffer
call DumpMem
call WaitMSg
exit
main ENDP
END main

几乎所有的Win32控制台函数都需要句柄(handle),其是一个32位无符号整数,用于唯一标识一个对象,例如一个位图,画笔或任何输入输出设备

输入abcdefg结果如下,发现插还有0Dh,0Ah,这是用户按下Enter产生的行结束符

Dump of offset 003B6000
-------------------------------
61 62 63 64 65 66 67 0D 0A

输出

等待已久的Hello World程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
INCLUDE Irvine32.inc
.data
endl EQU <0dh, 0ah> ;行结尾
message LABEL BYTE
BYTE "Hello World!", endl
messageSize DWORD ($ - message)

consoleHandle HANDLE 0 ;标准输出设备句柄
byteWritten DWORD ? ;输出字节数

.code
main PROC
;获取标准输出句柄
INVOKE GetStdHandle ,STD_OUTPUT_HANDLE ;标号,已经被定义过
mov consoleHandle, eax
;向控制台写一个字符串
INVOKE WriteConsole,
consoleHandle, ;控制台输出句柄
ADDR message, ;字符串指针
messageSize, ;字符串长度
ADDR byteWritten, ;返回输出字节数
0 ;未使用

call WaitMsg
INVOKE ExitProcess, 0
main ENDP
END main

动态内存分配

动态内存分配(dynamic memory allocation)又被称为堆分配(heap allocation)
C,C++,Java都有内置运行时堆管理器来处理程序请求的存储分配和释放。程序启动时,堆栈管理器常常从操作系统中分配一大块内存,并为存储块指针创建空闲列表(free list)。当接到一个分配请求时,堆管理器就把适当大小的内存标识为已预留,并返回指向该块的指针。
之后,当接收到对同一个快的删除请求时,对就会释放该内存块,并将其返回空闲列表。
例:使用动态内存分配创建并填充一个1000字节的数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
INCLUDE Irvine32.inc
.data
ARRAY_SIZE = 1000
FILL_VAL EQU 0FFh

hHeap HANDLE ? ;程序堆句柄
pArray DWORD ? ;内存块指针
newHeap DWORD ? ;新堆句柄
strl BYTE "Heap size is:", 0

.code
main PROC
INVOKE GetProcessHeap ;获取程序堆句柄(EAX返回)
.IF eax == NULL
call WriteWindowsMsg ;如果失败,显示消息
jmp quit
.ELSE
mov hHeap, eax
.ENDIF

call allocate_array
jnc arrayOK
call WriteWindowsMsg ;失败(CF=1),显示消息
call Crlf
jmp quit

arrayOK:
call fill_array
call display_array
call Crlf
;释放数组
INVOKE HeapFree, hHeap, 0, pArray

quit:
call WaitMsg
exit
main ENDP

allocate_array PROC USES eax
INVOKE HeapAlloc, hHeap, HEAP_ZERO_MEMORY, ARRAY_SIZE
.IF eax == NULL
stc ;进位标志置1
.ELSE
mov pArray, eax ;保存指针
clc ;进位标志置0
.ENDIF
ret
allocate_array ENDP

fill_array PROC USES ecx edx esi
mov ecx, ARRAY_SIZE
mov esi, pArray
L1:
mov BYTE PTR [esi], FILL_VAL
inc esi
loop L1
ret
fill_array ENDP

display_array PROC USES eax ebx ecx esi ;USES后面的寄存器不需要逗号隔开
mov ecx, ARRAY_SIZE
mov esi, pArray
L1:
mov al, [esi]
mov ebx, TYPE BYTE
call WriteHexB
inc esi
loop L1
ret
display_array ENDP
END main

下面是一项原型

1
2
3
4
5
6
7
8
9
HeapAlloc PROTO,
hHeap:HANDLE ;现有堆内存块的句柄
dwFlags:DWORD, ;堆分配控制标志
dwBytes:DWORD, ;分配的字节数

HeapFree PROTO,
hHeap:HANDLE,
dwFlags:DWORD, ;通常为0
lpMem:DWORD ;被释放内存块的指针

说明:hHeap通常由GetProcessHeap和HeapCreate初始化,dwFlags为标志值,常使用HEAP_ZERO_MEMORY将内存清0

X86 内存管理

我们重点关注的是存储管理的两个主要方面:

  • 将逻辑地址转为线性地址
  • 将线性地址转为物理地址(分页)

回顾

先回顾一下一些x86存储管理术语:

  • 多任务处理(multitasking)允许多个程序或任务同时运行。处理器在所有运行程序中划分其时间
  • 段(segment)是可变大小的内存区,用于让程序存放代码或数据
  • 分段(segmentation)提供了分隔内存区段的方法。它允许多个程序同时运行又不会相互干扰
  • 段描述符(segment descriptior)时一个64位的值,用于标识和描述一个内存段。它包含的信息有段基址,访问权限,段限长,类型和用法
  • 段选择符(segment selector)是保存在段寄存器(CS,DS,SS,ES,FS或GS)中的一个16位数值
  • 逻辑地址(logic address)就是段选择符加上一个32位的偏移量
    我们一直忽略段寄存器,因为用户程序从来不会直接修改这些寄存器,所以只关注了32位数据偏移量,但是,从系统来看十分重要,因为它们包含了对内存段的直接引用

逻辑地址转线性地址

线性地址

线性地址是一个32位整数,其范围为0FFFFFFFFh,它表示一个内存位置。如果禁止分页功能,那么线性地址就是目标数据的物理地址

逻辑地址转为线性地址

多任务操作系统运行多个任务在内存中同时运行,每个程序都有自己的唯一数据区。那这样,每个程序里都有一个变量的偏移量地址为200h会咋样?怎么区分?

x86使用一步或两步处理将变量偏移量转换为唯一的内存地址
1.段值+变量偏移量形成线性地址(linear address)
2.页转换(page translation)

分页

分页是x86的一个重要功能。

处理器初始只装载部分程序到内存,其他仍留在硬盘里。程序使用的内存被分割成若干小区域,称为页(page),通常一页大小为4KB。当程序运行时,处理器会选择内存中不活跃的页面替换出去,而将立即会被请求的也加载到内存里

操作系统通过维护一个页目录(page directory)和一组页表(page table)来持续跟踪当前内存中所有程序使用过的页面。当程序试图访问线性地址空间内的一个地址时,处理器会自动将线性地址转为物理地址。这个过程称为页转换(page translation)。如果不存在内存中,处理器中断产生一个页故障(page fault)。操作系统将被请求的页从硬盘复制到内存,然后程序继续执行。从应用程序来看,页故障和页转换都是自动发生的

Author

Ctwo

Posted on

2019-08-08

Updated on

2020-10-25

Licensed under

Comments