FPU (2) 一元二次方程式

本文介绍了一元二次方程的求根算法,并通过实例展示了如何利用8087 FPU指令实现求根过程。特别讨论了如何处理判别式小于零的情况,引入FPU条件跳转机制来判断实数解的存在性。


Ch 23 FPU (2) 一元二次方程式

這一章裏小木偶將延續上一章的指令,寫一個計算一元二次方程式的程式,ROOT.ASM。


一元二次方程式之根

底下小木偶就利用以上所提的 8087 指令求得一元二次方程式之根。根據國中所學的,假如有一個一元二次方程式 ax2+bx+c=0,則此方程式 x 之解為

一元二次方程式根之公式

底下這個程式是求 x2-25=0 方程式之解,如果您要求其他方程式的解,必須修改原始程式之的 lr_a、lr_b、lr_c 三係數 (在 MASM 6.x 裡,c 是保留字,所以為了使 MASM 5.x 與 6.x 都能順利組譯,我在係數之前加上 lr,表示長實數之意)。這個程式也只能由 SYMDEB.EXE 觀看結果,小木偶將於稍後再撰寫直接顯示 ST 之十進位數值於螢幕上的副程式。

;***************************************
code segment
assume cs:code,ds:code
org 100h
;---------------------------------------
start: finit ;---st--;-st(1)-;-st(2)-;-st(3)-;06
fld lr_b ; b ;
fmul lr_b ; b2 ; ; ; ;08
fld lr_a ; a ; b2 ;
fmul lr_c ; ac ; b2 ; ; ;10
fimul const ; 4ac ; b2 ;
fsubp st(1),st ;D=bb-4ac ; ; ;12
shr const,1
fsqrt ; SQR(D); ; ; ;14
fld lr_b ; b ; SQR(D)
fchs ; -b ; SQR(D); ; ;16
fld st ; -b ; -b ; SQR(D)
fadd st,st(2) ;-b+SQ(D) -b ; SQR(D); ;18
fild const ; 2 ;-b+SQ(D) -b ; SQR(D)
fmul lr_a ; 2a ;-b+SQ(D) -b ; SQR(D);20
fdiv st(1),st ; 2a ; x1 ; -b ; SQR(D)
fxch st(1) ; x1 ; 2a ; -b ; SQR(D);22
fstp x1 ; 2a ; -b ; SQR(D)
fxch st(1) ; -b ; 2a ; SQR(D); ;24
fsubrp st(2),st ; 2a ;-b-SQ(D)
fdivp st(1),st ; x2 ; ; ; ;26
fstp x2
int 20h ;28
org 160h ;29
lr_a dq 1.00 ;30
lr_b dq 0.00 ;31
lr_c dq -25.0 ;32
x1 dq ? ;33 其中之一根
x2 dq ? ;34 其中之一根
const dw 4 ;35
;---------------------------------------
code ends
;***************************************
end start

先來看看執行結果。

h:/homepage/source>e:symdeb root.com [Enter]
Microsoft (R) Symbolic Debug Utility Version 4.00
Copyright (C) Microsoft Corp 1984, 1985. All rights reserved.

Processor is [80286]
-G [Enter]

Program terminated normally (0)
-DL 160 L5 [Enter]
2117:0160 00 00 00 00 00 00 F0 3F +0.1E+1
2117:0168 00 00 00 00 00 00 00 00 +0.0E+0
2117:0170 00 00 00 00 00 00 39 C0 -0.25E+2
2117:0178 00 00 00 00 00 00 14 40 +0.5E+1
2117:0180 00 00 00 00 00 00 14 C0 -0.5E+1
-Q [Enter]

為了能夠方便觀察係數與根,所以小木偶在程式第 29 行加入『org 160h』,使得所有長實數變數能夠由位址 160H 開始,前三個長實數依次是二次方程式的係數,第四、五個長實數是兩根。在 SYMDEB 裏,『E』是表示 10 的冪方的意思,例如,位址 160H 的長實數是 +0.1E+1 就是表示 +0.1 乘以 10 的一次方,也就是 1 的意思。

至於為何我選在位址 160H 開始存放變數呢?其實 160H 這個位址的獲得經過假設再精確求得。該位址的限制是不能覆蓋程式碼,因程式不大,小木偶先假設位址為 1C0H,組譯連結後由 SYMDEB 觀察得知最後一行『INT 20H』在位址 157H,所以我再修改為 160H。

ROOT.ASM 程式有一個缺陷,假如 b2-4ac < 0,其平方根會產生錯誤,這時應使用 FPU 的比較指令進而產生條件跳躍,使程式轉向。

使 FPU 能條件跳躍

所謂條件跳躍就是比較兩數的大小,如果某數較大,就執行一段程式,若較小則執行另一段程式,類似這樣的跳躍稱為條件跳躍。在 FPU 的指令集並沒有能使 FPU 跳躍的指令,事實上 FPU 也無法改變 CPU 暫存器之值 (還記得要改變程式執行位址必須改變 CS:IP 之值),所以要產生 FPU 條件跳躍必須用間接方法。其步驟有四個:

  1. 先用 FPU 的比較指令改變 FPU 的狀態字組暫存器。
  2. 再用 FPU 的指令 FSTSW 把狀態字組存入一個記憶體變數裏。
  3. 將此記憶體變數存入 AH 暫存器,再用 CPU 指令 SAHF 指令存入 CPU 的旗標暫存器裏。此步驟使旗標暫存器之值等於狀態字組。
  4. 再用 CPU 指令 JL/JG/JE/JA/JB 來跳躍至正確處執行。

狀態字組

FPU 有五類暫存器,前章已介紹過最常用的堆疊暫存器,這裡將介紹狀態字組(status word)。顧名思義,狀態字組是一個 16 位元的暫存器,表示 FPU 的狀態,所謂狀態是指 FPU 是不是在忙碌中、是否除以零、是否無效運算、現在堆疊頂端是那一個堆疊暫存器等等,在此處我們要注意的是四個狀態碼位元,C0、C1、C2、C3。這些位 元分布如下圖:

8087 狀態字組

比較指令:FCOM、FCOMP、FCOMPP、FICOM、FICOMP

標題所見的這些比較指令都是以堆疊頂為目的運算元,而來源運算元可以是記憶體變數或是其他的堆疊暫存器,例如

FCOM    ST,ST(1)
FCOM ST,x

上述第一例是比較 ST 和 ST(1) 之數值,第二例是比較 ST 和記憶體變數 x 之數值,而這記憶體變數的形態可以是短實數和長實數。因為 FCOM 等指令均以 ST 為目的運算元,所以 ST 是可以省略不寫的,例如上述兩例,可以寫成下面的樣子,

FCOM    ST(1)
FCOM x

結果是一樣的。假如 ST 的比較對象是 ST(1) 的話( 來源運算元是 ST(1) ),連 ST(1) 也可以省略。至於 FCOMP 和 FCOMPP 分別是比較後彈出一次和彈出兩次,這裡彈出的數會消失不見並沒有存入記憶體中,這點和 FSTP 不同。而 FICOM 和 FICOMP 是用來比較整數的,而這來源運算元整數形態可以是字組整數和短整數。

FPU 的比較指令會改變狀態字組的 C3 和 C0 位元,C3、C0 位元是在狀態字組的第八、14 位元,如上圖。比較後依 ST 和來源運算元的大小,C3 和 C0 設定方式如下表:

  比較結果      C3  C0
----------------------------
ST>來源運算元 0 0
ST<來源運算元 0 1
ST=來源運算元 1 0

FSTSW 指令

FSTSW (store status word) 這個指令的功用是用來把狀態字組取出並存入AX暫存器或 16 位元長的記憶體變數裏,其語法是

FSTSW   mem16
FSTSW AX

注意!FSTSW 之後所接的運算原可以是 AX 或 16 位元長的記憶體變數,前者只能用在 387 等級以上的 FPU;至於 16 位元長的記憶體變數則是 8087 就可以使用了。為什麼只能接 AX 而不可接其他 16 位元的暫存器呢?主要是為了下面 SAHF 指令能很方便的把 AH 暫存器內的資料移到旗標暫存器裏,否則 FPU 的運算元是不能使用 CPU 的暫存器。

SAHF 指令

SAHF 是 8088 指令集中的一個指令,並非 FPU 指令。這個指令是把 AH 暫存器中的值移到旗標暫存器的較低的 8 個位元,這 8 個位元只有第七、六、四、二、零位元有用,第五、三、一位元沒有使用,請參考附錄二8088 的旗標暫存器

所以假如 AH 為 0100 0000B,則執行 SAHF 指令後,零旗標會被設為一,您可以預測看看下列程式片段將會有何結果?

        mov     ah,01000000b
sahf
jz zf_set
mov dx,offset mes1
jmp short print
zf_set: mov dx,offset mes2
print: mov ah,9
int 21h
mes2 db '零旗標已設定$'
mes1 db '零旗標未設定$'

這程式片段會顯示出『零旗標已設定』。所以事實上,跳躍指令其實是僅僅看旗標暫存器的設定值決定如何跳到那裡去執行,至於旗標暫存器如何設定,在跳躍指令執行時是不須理會的

FPU 的狀態暫存器裏的 C3 和 C0 位元,經 FSTSW 指令傳到 16 位元記憶體變數,再接著移到 AX 暫存器裏時,AH 的第 6、0 位元就是表示 C3 和 C0 位元,您可以對照上面 8087 狀態字組與 8088 旗標暫存器兩張圖,發現 C3 恰好對應 ZF,C0 恰好對應 CF。之後再由 SAHF 指令把 AH 移到旗標暫存器,所以 C3、C0 就移到零旗標與進位旗標了,只要檢查這兩個旗標就可以比較 ST 與來源運算元的大小,而決定跳躍方向。

修改 ROOT.ASM 程式

前面所撰寫的求一元二次方程式兩根的程式 ROOT.ASM 有缺陷,在這裡小木偶將他修改如果判別式小於零,程式能轉向執行。修改之後程式如下:

;***************************************
code segment
assume cs:code,ds:code
org 100h
;---------------------------------------
start: finit ;---st--;-st(1)-;-st(2)-;-st(3)-;06
fld lr_b ; b ;
fmul lr_b ; b^2 ; ; ; ;08
fld lr_a ; a ; b^2 ;
fmul lr_c ; ac ; b^2 ; ; ;10
fimul const ; 4ac ; b^2 ;
fsubp st(1),st ;D=bb-4ac ; ; ;12
shr const,1
fcom zero ;判別式是否小於 0 14
fstsw status ;結果存於 status 變數裏 15
fwait ;等待儲存完畢 16
mov ax,status ;比較結果移入 AX 17
sahf ;比較結果移入旗標暫存器 18
jb d_less_0 ;若小於則跳躍到 d_less_0 處 19
fsqrt ; SQR(D); ; ; ;20
fld lr_b ; b ; SQR(D)
fchs ; -b ; SQR(D); ; ;22
fld st ; -b ; -b ; SQR(D)
fadd st,st(2) ;-b+SQ(D) -b ; SQR(D); ;24
fild const ; 2 ;-b+SQ(D) -b ; SQR(D)
fmul lr_a ; 2a ;-b+SQ(D) -b ; SQR(D);26
fdiv st(1),st ; 2a ; x1 ; -b ; SQR(D)
fxch st(1) ; x1 ; 2a ; -b ; SQR(D);28
fstp x1 ; 2a ; -b ; SQR(D)
fxch st(1) ; -b ; 2a ; SQR(D); ;30
fsubrp st(2),st ; 2a ;-b-SQ(D)
fdivp st(1),st ; x2 ; ; ; ;32
fstp x2
int 20h ;34 結束程式
d_less_0: ;35 判別式小於零,無實數解
mov dx,offset message ;36 印出無實數解
mov ah,9
int 21h
int 20h ;39 結束程式

org 1e0h ;41
lr_a dq 1.00 ;42
lr_b dq 0.00 ;43
lr_c dq 25.0 ;44
x1 dq ? ;45 其中之一根
x2 dq ? ;46 其中之一根
zero dq 0 ;47
const dw 4 ;48
status dw ? ;49 狀態字組
message db '無實數解$'
;---------------------------------------
code ends
;***************************************
end start

將他存為 ROOT2.ASM,並組譯成 ROOT2.COM 檔,在 SYMDEB.EXE 裏執行並觀察結果。如果判別式為負值,則停止運算而印出『無實數解』後終止程式。

修改後的程式增加了幾行,第 14 到第 19 行,也就是在做開平方根運算前先檢查 ST 是否小於零,此時 ST 內的數值就是所謂的判別式 (b2-4ac)。值得注意的是第 16 行,小木偶加上了 FWAIT 指令,確保 FPU 已經把狀態字組存入 status 變數後,CPU 才將 status 之值存入 AX 裏。小木偶也試過如果把這行拿掉,似乎也能正確執行。


常數指令

FPU 裏有 7 個指令是用來把常用的常數推入堆疊頂稱為『常數指令』。這些常數都是內建在 FPU 裏的數值,都屬於暫時實數,其有效位數達 19 位數,當程式需要用到時可以很快的載入,節省時間與記憶體。這 7 個常數指令介紹如下:

FLD1

載入 1.0。( 把 1.0 推入堆疊頂 )

FLDZ

載入零。( 把 0.0 推入堆疊頂 )

FLDPI

載入圓周率,3.141592653589793239。( 把 π 推入堆疊頂 )

FLDL2T

載入 Log210。( 把 Log210 推入堆疊頂 )

FLDL2E

載入 Log2e,e 是自然對數的底數。( 把 Log2e 推入堆疊頂 )

FLDLG2

載入 Log102。( 把 Log102 推入堆疊頂 )

FLDLN2

載入 Loge2,事實上 Loge2 等於 Ln 2。( 把 Loge2 推入堆疊頂 )


註一:事實上有另一個指令可以直接比較 ST 和零,這個指令是 FTST (可以記成 TeST)。

FTST 指令

FTST 後不須接任何運算元,比較後狀態字組的設定方式和 FCOM 相同:

比較結果   C3  C0
----------------------------
ST>0 0 0
ST<0 0 1
ST=0 1 0

回到首頁到第二十二章到第二十四章
<think>我们正在处理用户关于用汇编语言实现求解一元二次方程根的问题。根据用户的问题,我们需要提供用汇编语言编写的求解一元二次方程的代码,并解释其原理。注意:用户提到了之前的引用,但那些引用是关于C语言和双二次方程的,与当前问题关联不大。因此,我们将专注于汇编语言的实现。一元二次方程的标准形:$ax^2+bx+c=0$,其中$a\neq0$。求根公:$$x=\frac{-b\pm\sqrt{b^2-4ac}}{2a}$$步骤:1.输入系数a,b,c(假设为浮点数)2.计算判别:$d=b^2-4ac$3.判断判别的值:-如果d<0,则没有实数根(但用户要求实数解,所以只处理实数根,若d<0则输出无实数解)-如果d>=0,则计算两个实数根:$x1=\frac{-b+\sqrt{d}}{2a}$,$x2=\frac{-b-\sqrt{d}}{2a}$4.输出结果(要求精确到小数点后6位)由于汇编语言实现浮点数运算和输入输出较为复杂,我们将使用x86汇编(基于Intel语法),并假设使用C标准库进行输入输出(即调用printf和scanf)来简化。注意:不同的汇编器(如NASM,GAS)语法略有不同,这里我们使用GAS(AT&T语法)在Linux环境下。我们将编写一个汇编程序,使用C标准库函数,因此需要链接libc。程序结构:.section.data输入输出格字符串.section.bss存储系数的空间.section.textmain函数调用scanf读取三个浮点数(a,b,c)计算判别判断判别,计算根调用printf输出结果(无实数解或两个根)注意:浮点数运算使用FPU指令(x87协处理器)或SSE指令。这里我们使用x87FPU。由于要求精确到小数点后6位,我们在输出格中指定%.6f。实现细节:-读取输入:使用scanf,格为"%f%f%f"-输出:若无实数根,输出字符串"Norealsolution",否则输出两个根(用空格分开)我们假设系数a不为0(用户输入保证),且为实数。下面是一个示例代码(使用GAS汇编,AT&T语法):</think>### 用汇编语言实现一元二次方程求根 要使用汇编语言求解一元二次方程 $ax^2 + bx + c = 0$ 的根,核心是计算判别 $d = b^2 - 4ac$ 并执行求根公 $x = \frac{-b \pm \sqrt{d}}{2a}$。以下是实现步骤: #### 关键步骤 1. **输入处理** 通过寄存器或内存位置获取系数 $a, b, c$(需转换为浮点格) ```assembly ; 假设系数存储在内存中 a dd 1.0 ; 示例值 b dd -5.0 ; 示例值 c dd 6.0 ; 示例值 ``` 2. **计算判别** 使用 x87 FPU 指令计算 $d = b^2 - 4ac$: ```assembly fld dword [b] ; 加载 b 到 FPU 栈 fmul st0, st0 ; 计算 b&sup2; fld dword [a] ; 加载 a fmul dword [c] ; 计算 a*c fadd st0, st0 ; 2ac fadd st0, st0 ; 4ac (2ac+2ac) fsubp st1, st0 ; d = b&sup2; - 4ac ``` 3. **判别判断** 检查 $d$ 的值: ```assembly ftst ; 比较 d 与 0.0 fstsw ax ; 存储状态字 sahf ; 传输到 EFLAGS jb no_real_roots ; 若 d < 0 跳转 ``` 4. **计算实数根** 若 $d \geq 0$,计算两个根: ```assembly fsqrt ; √d fld dword [b] fchs ; -b fld st0 ; 复制 -b fadd st0, st2 ; -b + √d fdiv dword [a] ; /(2a) -> x1 fstp qword [x1] ; 存储 x1 fsub st0, st2 ; -b - √d fdiv dword [a] ; /(2a) -> x2 fstp qword [x2] ; 存储 x2 ``` 5. **输出处理** 将结果转换为字符串输出(需调用系统 API 或自定义格化) #### 完整示例(NASM 语法片段) ```assembly section .data a dd 2.0 b dd -11.0 c dd 5.0 x1 dq 0.0 x2 dq 0.0 fmt db "根: x1=%.6f, x2=%.6f",0 section .text global _start _start: ; 计算判别 (b&sup2; - 4ac) fld dword [b] fmul st0, st0 fld dword [a] fmul dword [c] fadd st0, st0 fadd st0, st0 fsubp st1, st0 ; 检查判别 ftst fstsw ax sahf jb .no_solution ; 计算根 fsqrt fld dword [b] fchs fld st0 fadd st0, st2 fdiv dword [a] fstp qword [x1] fsub st0, st2 fdiv dword [a] fstp qword [x2] ; 输出结果 (需实现print函数) jmp .exit .no_solution: ; 处理无实数解情况 .exit: mov eax, 1 int 0x80 ``` #### 技术难点 1. **浮点精度控制** 需使用 `FNINIT` 初始化 FPU,并通过精度控制位确保 6 位小数精度: ```assembly fninit fldcw [control_word] ; 设置舍入模 ``` 2. **特殊输入处理** - 当 $a=0$ 时退化为线性方程 - 判别为负时输出复数解(需额外计算虚部) - 处理 NaN/无穷大等异常值 3. **性能优化** - 使用 SSE 指令替代 x87(现代 CPU) - 并行计算 $\frac{-b}{2a}$ 和 $\frac{\sqrt{d}}{2a}$ 再组合 此实现展示了汇编语言直接操作硬件浮点单元的能力,但需注意不同架构(x86、ARM)的指令集差异[^1]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值