文章目录
万丈高楼平地起
运行截图
使用的语言环境
汇编语言为intel 8086机上的16位语言。
采取dosbox模拟实地址模式的8086机。
汇编器为MASM-v6.11
以VScode作为编辑器。
全部代码
assume cs:code, ds:data, ss:stack
data segment
buffer dw 16 dup(0)
CRLF db 13, 10, '$'
tip0 db 'Please input the length of numbers you want to sort(maximum is 16):', 13, 10, '$'
tip1 db 'Please input the number you want to sort:(each number end with a non-digital number):', 13, 10,'$'
tip2 db 'Below is the sorted numbers:', 13, 10, '$'
data ends
stack segment
dw 32 dup(0)
stack ends
code segment
start:
mov ax, data
mov ds, ax
mov ax, stack
mov ss, ax
mov sp, 40h
mov dx, offset tip0
mov ah, 9
int 21h
call input
mov cx, ax
;12a -> c
;23b -> 17
;9c -> 9
;45d -> 2d
;8e -> 8
mov dx, offset tip1
mov ah, 9
int 21h
mov si, offset buffer
call inputSomeData
;mov di, 2
;call partition
call quickSort
mov dx, offset tip2
mov ah, 9
int 21h
call outputSomeData
mov ah, 4ch
int 21h
quickSort:
;select the 0-th
;select base number less put in the left bigger in the right
;input : cx = length of nums; ds:si = the first address of array
;return : ds:si = the first address of sorted array
;--calcuate first and last
push si
push di
push cx
;-------------
shl cx, 1; word byte
add cx, si
mov di, cx
sub di, 2
call quickSortLeftRight
;-------------
pop cx
pop di
pop si
ret
quickSortLeftRight:
;input : ds:si = the first address of array; ds:di = the last address of array;
;return : ds:si = the first address of sorted array
;
push si
push di
push bp
;----------------
;---if si >= di then quit
cmp si, di
jnb endQuickSortLeftRight
;---------
call partition
;bp is the bias place
;
;-----------
push si
push di
;si - bp-2------------
sub bp, 2
mov di, bp
call quickSortLeftRight
pop di
;bp+2 - di------------
add bp, 4
mov si, bp
call quickSortLeftRight
pop si
;----------------
endQuickSortLeftRight:
pop bp
pop di
pop si
ret
partition:
;
;input : ds:si = the first address of array; ds:di = the last address of array
;return : bp = the address of bias
push si
push di
push cx
push bx
push ax
;----------
mov bx, word ptr ds:[si]
;
mov bp, si; record the address of bias number
add si, 2;
;
;if only have one element
cmp bp, di
jz endPartition
;----------
LeftLessRight:
findLeft:
mov ax, word ptr ds:[si]
cmp ax, bx
jnb endFindLeft
add si, 2
;-----------if out of size-
cmp si, di
ja endPartition
;-------------------------
jmp findLeft
endFindLeft:nop
findRight:
mov ax, word ptr ds:[di]
cmp ax, bx
jna endFindRight
sub di, 2
;------------------if out of size
cmp si, di
ja endPartition
;-------------------------------
jmp findRight
endFindRight:nop
;--------avoid swap, si > di
cmp si, di
jnb endPartition
;swap two number
mov ax, word ptr ds:[si]
mov cx, word ptr ds:[di]
mov word ptr ds:[di], ax
mov word ptr ds:[si], cx
;
;add si, 2
;sub di, 2
cmp si, di
jna LeftLessRight
;-----------
endPartition:
;-------swap the bais number
mov ax, word ptr ds:[di]
mov word ptr ds:[di], bx
mov word ptr ds:[bp], ax
;----------
mov bp, di
;----------
pop ax
pop bx
pop cx
pop di
pop si
ret
inputSomeData:
;function : read cx number from keyborad, end with nondigital charter
;input : cx = number of input, ds:si = the first address of store number
;return : void
push ax
push cx
push dx
push si
;-----------------
lea dx, CRLF
inputSomeDataLoop:
call input
mov word ptr ds:[si], ax
mov ah, 9
int 21h
add si, 2
loop inputSomeDataLoop
;----------------
pop si
pop dx
pop cx
pop ax
ret
outputSomeData:
push cx
push dx
;-----------------
lea dx, CRLF
outputSomeDataLoop:
mov ax, word ptr ds:[si]
call output
mov ah, 9
int 21h
add si, 2
loop outputSomeDataLoop
;------------------
pop dx
pop cx
ret
input:
;function : read number from keyborad until input charter isn't digital charter
;input : void
;output : ax <- input number
;
;压栈
push dx
push cx
push bx
;读取字符 放
mov cl, 10
mov bx, 0
read:
mov ah, 1
int 21h
cmp al, '0'
jb quit;如果dl小于0
cmp al, '9'
jg quit;如果比9大
mov dl, al
mov ax, bx
mul cl
sub dl,'0'
mov dh, 0
add ax, dx
mov bx, ax
jmp read
quit:
;输出结果
mov ax, bx
;出栈
pop bx
pop cx
pop dx
ret
output:
;function : output ax as decimal based number
;input : ax
;return : void
;压栈
push dx
push cx
push bx
push ax
;------------
mov bx, 0
mov cx, 10
mov dx, 0
div cx
divLoop:
push dx
mov dx, 0
div cx
inc bx
cmp dx, 0
jnz divLoop
outputLoop:
pop dx
mov dh, 0
add dl, 30h;ascii
;char
mov ah, 2
int 21h
;
dec bx
cmp bx, 0
jnz outputLoop
;------------
;出栈
pop ax
pop bx
pop cx
pop dx
ret
code ends
end start
整体思路
输出提示
获得将要输入的数据个数 (过程实现)
输出提示
将用户给定的数据放入内存 (过程实现)
进行快速排序 (过程实现)
输出提示
输出结果(过程实现)
返还控制权
mov ax, data
mov ds, ax
mov ax, stack
mov ss, ax
mov sp, 40h
mov dx, offset tip0
mov ah, 9
int 21h
call input
mov cx, ax
;12a -> c
;23b -> 17
;9c -> 9
;45d -> 2d
;8e -> 8
mov dx, offset tip1
mov ah, 9
int 21h
mov si, offset buffer
call inputSomeData
;mov di, 2
;call partition
call quickSort
mov dx, offset tip2
mov ah, 9
int 21h
call outputSomeData
mov ah, 4ch
int 21h
input实现
规定接口
input 即从输入中不断读取数字字符,直到用户输入非数字字符。
最后返回用户输入的数据
即
输入123b
返回123
实现思路
从输入中读取数据,有两种方式。
一种是调用OS的中断,2号函数读取字符放到al中。
另外一种是利用BIOS的中断,从键盘的输入缓冲区读取数据。(int 9, 60端口读取数据)
此处为了简便,采取OS的中断。
接下来需要考虑的就是如何计算出正确的数值?
由于每次都需要用ax去触发OS的中断,所以把中间的结果放在ax会很麻烦,需要不断注意是否修改了数据,导致了错误。
所以在这里采取bx作为中间结果的存储。而将ax用于最后的结果返回。
但是除了之前的结果,还需要去计算当前的结果,由于mul运算只能用ax去做。
所以还需要一个寄存器用于保存之前ax的值,这里采用dx。
mul运算还需要一个操作数,只剩下CX了,所以给cl赋值10。
然后每次触发中断,判断al是否是数字字符,是就进行运算,不然的话就返回当前的结果。
实现代码如下:
input:
;function : read number from keyborad until input charter isn't digital charter
;input : void
;return : ax <- input number
;
;压栈
push dx
push cx
push bx
;读取字符 放
mov cl, 10
mov bx, 0
read:
mov ah, 1
int 21h
cmp al, '0'
jb quit;如果dl小于0
cmp al, '9'
jg quit;如果比9大
mov dl, al
mov ax, bx
mul cl
sub dl,'0'
mov dh, 0
add ax, dx
mov bx, ax
jmp read
quit:
;输出结果
mov ax, bx
;出栈
pop bx
pop cx
pop dx
ret
output实现
规定接口
将ax寄存器中的数按照十进制的格式输出
实现思路
div reg
有两种格式。
一种是8位的,商放al,余数放ah
一种是16位的,商放ax,余数放dx
此时任务已经明确,即将ax中的数据按照一个格式输出,机器又提供了16位div的功能。而且用8位会出现溢出的情况,比如2560,就算得不对。(可以避免,代码放后面)
那就别折磨自己了。
用一个计数器,bx,cx都行,我这里用的是bx。记录运算过程中余数的个数。
至于具体的运算,每次将dx清为0, 然后再除cx=10,那么我们要的余数很自然的就放到了dx中,ax中存的还是商。
不用管那么多烦心事。
最后将栈里面的余数输出即可。
div 16
代码如下:
output:
;function : output ax as decimal based number
;input : ax
;return : void
;压栈
push dx
push cx
push bx
push ax
;------------
mov bx, 0
mov cx, 10
mov dx, 0
div cx
divLoop:
push dx
mov dx, 0
div cx
inc bx
cmp dx, 0
jnz divLoop
outputLoop:
pop dx
mov dh, 0
add dl, 30h;ascii
;char
mov ah, 2
int 21h
;
dec bx
cmp bx, 0
jnz outputLoop
;------------
;出栈
pop ax
pop bx
pop cx
pop dx
ret
div 8
这里提供一个除法不会溢出的代码
ax中放被除数
cl中放除数
运行结束后
ax中放商
cl中放余数
在此稍微解释一下原理,第一次看容易晕。
应用了一个数学公式:
X/N = int(H/N) * 65536 + (REM(H/N) * 65536 + (L/N))
H表示X的高8位,L表示低8位。
int()表示取整,直接丢弃小数点后的数据。
rem()表示取模
这个公式其实也很好理解。
我们把x当成一个两位数。不过进制是65536进制。
那么按照算除法的经验,就是第一个除,算出来结果,然后得到的余数跟还剩下的位数进行组合
再进行计算。
不过就算理解了为什么这么做,下面的代码可能看起来也很迷。
在此简要说明,按照上述公式。为了得到最后的结果,我们应该先计算高位除除数的余数和商。
所以我们先保存了低位(push ax)。
然后计算了高位的余数和商(mov al, ah, mov ah, 0 div cl)
我们将此时得到的结果再度保存起来(mov dx, ax)
取出低位(pop ax)
按照计算公式我们需要拿到高位运算后的结果,这个结果目前保存在dh中.
我们取出后再次进行计算(mov ah, dh div cl)
得到的余数放在ah,商放在al。
为了符合我们定义的接口。将余数放cl(mov cl, ah)
之前计算得到的商放到ax的高位(mov ah, dl)
返回即可
读者要是认为自己理解了上述过程,可以去写写32/16的不溢出的除法。
divdb:;16/8 not overflow
;
push dx
;--------------------
push ax
mov al, ah
mov ah, 0
div cl
mov dx, ax
pop ax
mov ah, dh
div cl
mov cl, ah
mov ah, dl
;------------------------
pop dx
ret
outputSomeData & inputSomeData
接口
inputSomeData:
从输入中读取cx个数,放到ds:si为首地址的内存段中
outputSomeData
从内存段ds:si,输出之后的CX个数
实现
多次调用之前的output 和 input即可做到。
比较简单。
中间的mov ah, 9 int 21h 是为了换行写的
不然堆一块太难看了。
代码如下:
inputSomeData:
;function : read cx number from keyborad, end with nondigital charter
;input : cx = number of input, ds:si = the first address of store number
;return : void
push ax
push cx
push dx
push si
;-----------------
lea dx, CRLF
inputSomeDataLoop:
call input
mov word ptr ds:[si], ax
mov ah, 9
int 21h
add si, 2
loop inputSomeDataLoop
;----------------
pop si
pop dx
pop cx
pop ax
ret
outputSomeData:
push cx
push dx
;-----------------
lea dx, CRLF
outputSomeDataLoop:
mov ax, word ptr ds:[si]
call output
mov ah, 9
int 21h
add si, 2
loop outputSomeDataLoop
;------------------
pop dx
pop cx
ret
快速排序
伪码描述
//划分
int partition(int a[], int left, int right) {
int begin = left;
int bias = a[left++];//此处有多种选取方式 简单起见 选择数组的第一个元素
while(left < right) {
while(left <= right && a[left] < bias) ++left;//找到第一个大于等于的元素
while(left <= right && a[right] > bias) ++right;//找到第一个小于等于的元素
if(left < right) {//此处是为了防止交换错误
swap(a[left], a[right]);
}
}
//上述指针移动过程中必须要停和基准值相同的值 不然对于如下数据就会出现错误 2 1 1 2 1 1 3 4 5 2 6
swap(a[begin], a[right]);//把基准放到中间
return right;//返回基准的位置 便于判断
}
//实际快排函数
void quickSort(int a[], int left, int right) {
if(left < right) {//一个元素不用再处理 超过了无法再处理
int mid = partition(a, left, right);
quickSort(a, left, mid - 1);
quickSort(a, mid + 1, right);
}
}
//快排接口
void quickSort(int a[], int length) {
quickSort(a, 0, length - 1);
}
我们首先来实现最简单的快排接口 - quickSort。
调函数就行
这里需要注意的是如何计算左右指针的值。
这里的左指针为si, 右指针为di
长度为cx。
但是当前数据的长度为2byte。计算机按照字节寻址。
因为长度为cx的数据离起始点的偏移量为 2*cx - 2
即cx左移一位(乘2)再减去2
quickSort:
;select the 0-th
;select base number less put in the left bigger in the right
;input : cx = length of nums; ds:si = the first address of array
;return : ds:si = the first address of sorted array
;--calcuate first and last
push si
push di
push cx
;-------------
shl cx, 1; word byte
add cx, si
mov di, cx
sub di, 2
call quickSortLeftRight
;-------------
pop cx
pop di
pop si
ret
接下来再来看递归调用的函数quickSortLeftRight
首先进行长度判断,如果left < right 就继续,否则直接退出。
汇编实现类似于
if(){}不是太好实现。
因此用的是相反的逻辑,即left >= right,就直接退出。
之后进行划分。
划分元素所在的位置我们放到了bp中。
然后就该进行递归调用了
当时我为了防止递归调用出现问题,每次都加了一个条件判断。
只有在合理的范围内才让调用。
后来仔细一想,其实没必要。
但是懒得删了。
注意这里我们要先保存si和di的值。(push si push di)
因为我们会修改si和di的值,用于递归函数的左右边界确定。
同理,对右侧的调用也是如此。
然后我们返回。
quickSortLeftRight:
;input : ds:si = the first address of array; ds:di = the last address of array;
;return : ds:si = the first address of sorted array
;
push si
push di
push bp
;----------------
;---if si >= di then quit
cmp si, di
jnb endQuickSortLeftRight
;---------
call partition
;bp is the bias place
;
;-----------
push si
push di
;si - bp-2------------
sub bp, 2
mov di, bp
call quickSortLeftRight
pop di
;bp+2 - di------------
add bp, 4
mov si, bp
call quickSortLeftRight
pop si
;----------------
endQuickSortLeftRight:
pop bp
pop di
pop si
ret
最后是划分函数-partition
首先我们明确接口,
si是起始,di是终点。
bp是划分标准在的位置。
我们首先拿到划分值(mov bx, word ptr ds:[si])
保存 起始点bp
然后左指针右移一位
此时我们需要去比较一下此时两个指针的大小,是否还满足si <= di
如果不满足,说明只有一个元素,不用进行划分。
(因为汇编的循环是一种do-while,不判断的话,结果会出问题)
然后左指针不断右移找大于等于的数据,没找到,则左指针继续右移,然后判断是否超过了右指针
超过了说明已经全部满足了条件,直接退出即可。
不然的话,继续找,当找到满足条件的值后,退出当前循环。
右指针左移也是同样的思路。不再赘述。
之后判断左右指针的相对位置,判断是否需要交换元素值。
退出的时候,交换基准和右指针的位置,将右指针的位置赋值给bp。
返回即可。
partition:
;
;input : ds:si = the first address of array; ds:di = the last address of array
;return : bp = the address of bias
push si
push di
push cx
push bx
push ax
;----------
mov bx, word ptr ds:[si]
;
mov bp, si; record the address of bias number
add si, 2;
;
;if only have one element
cmp bp, di
jz endPartition
;----------
LeftLessRight:
findLeft:
mov ax, word ptr ds:[si]
cmp ax, bx
jnb endFindLeft
add si, 2
;-----------if out of size-
cmp si, di
ja endPartition
;-------------------------
jmp findLeft
endFindLeft:nop
findRight:
mov ax, word ptr ds:[di]
cmp ax, bx
jna endFindRight
sub di, 2
;------------------if out of size
cmp si, di
ja endPartition
;-------------------------------
jmp findRight
endFindRight:nop
;--------avoid swap, si > di
cmp si, di
jnb endPartition
;swap two number
mov ax, word ptr ds:[si]
mov cx, word ptr ds:[di]
mov word ptr ds:[di], ax
mov word ptr ds:[si], cx
;
;add si, 2
;sub di, 2
cmp si, di
jna LeftLessRight
;-----------
endPartition:
;-------swap the bais number
mov ax, word ptr ds:[di]
mov word ptr ds:[di], bx
mov word ptr ds:[bp], ax
;----------
mov bp, di
;----------
pop ax
pop bx
pop cx
pop di
pop si
ret
遇到的一些bug
之前我怕递归调用层数太多,栈不够用。于是就扩充了一下栈的位置,修改了栈的指针。
但是之后看栈的空间还有富裕。于是就又修改了栈的大小。
但是忘记改栈指针的大小了。
于是在我之后的操作中,会发现程序返回时的代码很奇怪。不是之前的代码,debug了好久。
查不出来问题。就当我快放弃的时候,看到了这个sp。
意识到是栈空间和栈指针的不匹配的问题。导致栈指针修改的内存空间是我的代码段的内存空间。
每次出栈和入栈,修改了程序段的数据。
所以返回的代码不对。
改完之后就正常运行了。
剩下的bug多是在调试快速排序的代码中遇到的。是逻辑bug。
不过通过单步调试,都解决了。
唯有这个栈指针错误的代码让我记忆深刻。
(因为调试的时候,mov ss, ax,后再执行一条指令 这是为了防止sp和ss所指定的内容不一样,比如这个时候发生中断。所以我将mov sp, 20h的代码写到了后面。导致我调试的时候都忽略了这个细节)
毕竟谁没事看sp。