附錄一 浮點數


緣起

最近幾個月 ( 民國 106 年 10 月 ),小木偶在搞有關質數的程式,但小木偶所學有限,加以並非科班出身,故所寫的程式不是最精簡,也不是最有效率。即使雖不能至,但仍心嚮往之。為了要計算質數列表的副程式,會花去多少時間,必須計算到一秒以下的時間;尤其是當所需求的質數不大時,例如求 100 以內的質數列表,所花時間更短,甚至在微秒 ( 10-6 秒 ) 以下。因此必須要有一副程式能顯示浮點數,其功能類似 wsprintf API。也就是說,能把浮點數以 ASCII 字串或萬國碼字串表示。但小木偶資質駑鈍,上窮碧落下黃泉,搜索多時,仍無結果。於是便興起一個想法,何不自己寫一個?另外,既然打算撰寫把浮點數轉換成字串的副程式,那相反的過程,何不順便也寫好?於是就有了這個附錄。

也幸好小木偶雖不年輕,但也還不至老到患有癡呆症,想起其實在 DOS 時代,已經寫過一個類似的程式。現在只需要參考以前的「筆記」,再把她改成 Win64 版本即可。所以這一個附錄,其實參考「DOS 組合語言內容」的「FPU(1) 簡介」、「FPU(2) 二次方程式」、「FPU(3) 指數」、「FPU(4) IEEE 與 BCD」。


原理

把暫時實數變成 ASCII 或萬國碼字串的副程式:st0_to_sn

先說說此附錄要實作之副程式功能:把 FPU 堆疊頂,即 ST 暫存器的暫時實數 ( 80 位元表示的實數 ),變成以科學記號表示的阿拉伯數字字串,存入指定的記憶體中。小木偶想,這個副程式姑且稱為 st0_to_sn。st0 指的是 FPU 最上面的堆疊暫存器,即 ST,sn 指的是科學記號 ( scientific notation ) 的意思,所謂科學記號,就是把一數變成像 a×10n 形式的數,n 稱為十的冪方數,為一整數,可以是正數,也可以是負數;a 稱為係數 ( mantissa ) 或稱有效數 ( significand ),其範圍是 10>a≧1。把一個數變成科學記號的形式,只有一種結果。換句話說,數值與科學記號是一對一的關係。言歸正傳,繼續說 st0_to_sn 的原型,為

st0_to_sn   PROTO   pBuffer:LPSTR,flag:QWORD

先講第二個參數,flag。顧名思義,它是一個旗標,指定如何輸出科學記號,各個位元所代表的意義如下:

再來說明第一個參數,pBuffer,它是記憶體位址,此記憶體存放經 st0_to_sn 計算後,所得的科學記號。如果 flag 指定為 ASCII 碼,此記憶體至少要有 27 個位元組。下圖是以「-96485.33289」為例,表示經過 st0_to_sn 處理過後,變為科學記號是「-9.648533289×104」,儲存在記憶體中的情形:

FPU 所存入的有效位數,最多有 18 位,再加上符號、小數點及結尾的空字元 ( Null ),共 21 個位元組;此外,FPU 能表達的暫時實數,從 10-4932 到 104932,故需要四個位元組存放數字,再加上符號及結尾的空字元,共需 6 個位元組放置 10 的冪方數 ( exponential )。亦即第 0∼20 個位元組用於存放係數;第 21∼26 個位元組用於存放十的冪方 ( 或稱十的次方數 )。如果 flag 指定萬國碼,此記憶體至少要有 54 個位元組。前 42 個位元組放置係數,後 12 個位元組放置 10 的冪方數。亦即第 0∼41 個位元組用於存放係數;第 42∼53 個位元組用於存放十的冪方。

求暫時實數的係數與冪方

現在問題來了,要怎麼求出係數與十的冪方數呢?小木偶打算先說十的冪方怎麼算,因為十的冪方求出之後,才能算出係數來。十的冪方的算法,其實很簡單,只要利用以 10 為底的對數很容易就算出來了。

log10(a×10x)=log10a+log1010x=log10a+x

其整數部份,x,就是冪方數,問題是 FPU 指令堙A並沒有求以 10 為底的對數值,只有求以 2 為底的對數值,即 FYL2X,不過還好小木偶還記得國中老師教的,用「換底公式」可以計算出來,換底公式是:

FYL2X 會計算 ST(1)×log2ST 之值,再把結果存入 ST(1),最後彈出一次堆疊。至於 log210 之值,可以用另一個載入常數的 FPU 指令,FLDL2T,取得。兩者相除,再「向負無窮大」捨去,就能得到十的冪方數。求出十的冪方數之後,再求係數就不難了。其實您可以想像,係數就是原來的值除以 10x。例如 123 寫成科學記號是 1.23×102,log 123≒2.0899,像負無限大捨去為 2,就是十的冪方,如果把 123÷102,所得到的就是係數,1.23。但是 FPU 並沒有指令能夠把帶有小數的數值,以字串方式存入記憶體內,所以無法把 1.23 變成字元,存到記憶體堙C如果有的話,也不必寫 st0_to_sn 副程式了。但是,FPU 倒是有個 FBSTP 指令,能把 ST 的整數部份,以聚集的 BCD 方式存入記憶體堙C有關 FBSTP 的說明,請參考

但是還得考慮另一個因素。係數有幾位有效數,還必須由 st0_to_sn 的 flag 參數指定位數。舉個例子來說,如果要把「-96485.33289」變成科學記號,且要有 8 位有效位數,其結果應該是「-9.6485333×104」。剛才提過,FPU 只有 FBSTP 指令能把整數變成聚集的 BCD 數存於記憶體堙A因此必須把-96485.33289 乘上 103,變成-96485332.89 後,再四捨五入,就能得到係數,-96485333再調整小數點即可。但是這 103 的「3」次方是怎麼來的呢?顯然是 8 位有效位數減去十的冪方數,4,再減去一。減去一是因為三位數時,只有 102;四位數時,為 103,故 8 位數時,應該是十的 7 次方,所以還要再減去一。

小木偶再舉一個例子說明,可能會更清楚一些。如果想以有效位數 5 位,轉換 1.60217662×10-19,步驟如下:

  1. 求出十的冪方數:log101.60217662×10-19≒-18.79529,向「負無限大」捨去之後為「-19」。
  2. 以 FBSTP 指令,把「-19」存入記憶體中,即為十的冪方數,在記憶體內為「19 00 00 00 00 00 00 00 00 80」。
  3. 5-(-19)-1=23。
  4. 1.60217662×10-19×1023=16021.7662
  5. 用 FRNDINT 指令,四捨五入至整數位,變成 16022。
  6. 再以 FBSTP 指令存於記憶體,該記憶體內容為「22 60 01 00 00 00 00 00 00 00」。
  7. 把加上小數點,使之成為有效數。

求 2X 的副程式:two_p_x

雖然解決了有效數與冪方數的問題,但也產生了新的問題。這個問題就是,上面的例子堬 4 個步驟,要計算十的幾次方的數,那麼,要如何去計算呢?在 FPU 指令堙A不僅沒有計算 XY 的指令,也沒有計算 10X 的指令,僅有能計算 2X 的兩條指令:FSCALE 和 F2XM1。

好吧,小木偶的想法是,先撰寫一個能夠求出 2X 的副程式來,這個副程式稱為 two_p_x,而 X 可以是任意數。完成這個副程式之後,再利用 FPU 的指令,FYL2X 及數學公式:

XY=2log2XY=2Y×log2X

就能寫出可以求 XY 的副程式。當 X=10,就能求出 10 的幾次方的數了。經過上述的說明,各位讀者應當明白,要把 ST 暫存器的暫時實數,變成以科學記號表示的阿拉伯數字字串,存入指定的記憶體中,此過程中會要去計算 XY 的數值,小木偶把它寫成一個副程式,x_p_y;而在 x_p_y 副程式中,會經過計算 2X 的的過程,小木偶也把它寫成副程式,two_p_x。也就是說,欲達此目的,必須先後撰寫出兩個副程式:two_p_x 及 x_p_y。底下就先說明 two_p_x 的計算過程。

FSCALE 會先把 ST(1) 內的數值以向 0 捨去的方式變成整數,然後計算 ST×2ST(1),再把其結果存入 ST 堙A而 ST(1) 之值不變。F2XM1 則是計算 2ST-1 之值,然後再存回 ST 暫存器堙A原先的 ST 必須在 -1 與 1 之間,如果超出這範圍,所得結果為未定義。看了有關 FSCALE 與 F2XM1 的說明,您應當發現,如果真要計算 2X,而 X 是任意數的話,還真無法單單靠一條 FPU 指令就能完成,必須經過好幾個步驟才行。

這時候,又得靠數學的指數律,才能做到:

2X=2i+f=2i×2f

小木偶的想法是,把任意數,X,分成整數部份及小數部份,分別以 i 和 f 表示,亦即 X=i+f。這樣就可以用 F2XM1 計算 2f-1,再加上一,就能得到 2f。然後再把 2f 放在 ST,i 放在 ST(1),執行 FSCALE 就能得到 2X 了。

至於要怎麼把任意數分成小數部份與整數部份呢?有條 FPU 指令,FRNDINT 可以達成目的。但是 FRNDINT 捨去方法有四種,要使用哪一種才行呢?考慮到 2X,X 可能是正數,也可能是負數,故有兩種情形:

  1. 第一種情形,指數為正數,例如 25.11,顯然指數可以分成 5+0.11。
  2. 第二種情形,指數為負數,例如 2-5.11,指數可以分成 -6+0.89。

如果 FRNDINT 是向負無窮大捨去,而小數部份是原指數減去向負無窮大捨去後的整數部份,那麼這兩種情形其實是一樣的,就能節省程式碼。以第一種情形而言, 5.11 向負無限大捨去,所得整數是 5;原指數是 5.11,減去 5,得差為 0.11。以第二種情形來說,-5.11 向負無窮大捨去後所的整數為 -6,原指數是 -5.11,減去 -6,得到的是 0.89。

求 XY 的副程式:x_p_y

如果已經寫好了 two_p_x 副程式,那麼就可以利用底下的數學公式計算 XY

XY=2log2XY=2Y×log2X

在數學上,真數 ( log2X 中,2 是底數,X 是真數 ) 必定是正數,但是其實我們知道,在 XY 堙AX 可以是 -2,Y 是 3,這樣就變成 -8。不過當 Y 不是整數時,底數 X 就不可以是負數。另外,數學上也定義 00 數值未定。因此小木偶整理了下表,下表塈漇數與底數分別區分城正數、0、負數各三種情形,兩兩組合,得到 9 種組合情形:

XY1 XY
0未定值 無意義
若 Y 為整數,才能求 XY 之值,否則無意義 1若 Y 為整數,才能求 XY 之值,否則無意義

經由上表分析,得知可以在一進入 x_p_y 時,先檢查底數 ( 即 ST 之值,上表中的 X ) 為 0、正數或負數。可分底下三種情形:

  1. 如果底數為正數,那就很單純,直接執行 FYL2X 指令,再呼叫 two_p_x 副程式即可。
  2. 如果底數是零的話,再檢查 ST(1) 是否為正數,如果為正數,表示計算零的正數次方,結果就是 0,所以傳回 0 並清除進位旗標就可以了;如果 ST(1) 為 0 或負數,設定進位旗標,表示無意義或未定值。
  3. 如果底數是負數,就得檢查指數是否為整數,檢查的方法是把指數拷貝一份,向負無限大捨去,再跟原指數比較,如果相同,則為整數,否則為非整數。如果是整數,能夠求出 XY,但要注意指數是偶數,XY 為正值;反之為負值。

檢查 ST 之值為正數、負數或零的方法,可以參閱 DOS 組合語言的第 23 章其註解

把 ASCII 或萬國碼字串變成暫時實數的副程式:str_to_st0

這個副程式是把以 0 結尾的字串,變成暫時實數,並存於 ST(0) 暫存器堙C其原型為

str_to_st0      PROTO   pStr:QWORD,unicode:QWORD

先說第二個參數,unicode,這個參數是表明字串種類:如果是 ASCII 字串,unicode 為 0;如果是萬國碼字串,unicode 為 1。第一個參數,pStr,是字串位址,包含結尾的 0,如果是ASCII碼,最大長度是 27 個位元組;如果這個字串為萬國碼,最大長度為 54 個位元組。這個字串能接受的字元,只有「0」∼「9」、小數點、正負號和「E」或「e」等 15 個字元,其餘字元都是不合法的。「E」或「e」代表 10 的幾次方,「E」或「e」後面接著的,就是 10 的次方數。除此之外,當然還有些限制,這些限制主要來自數學上的限制。例如:

  1. 10 的幾次方數只能是整數。
  2. 正負號只能在第一個字元,或是緊接著「E」或「e」後面的那個字元。
  3. 小數點只能有一個,而且不能在「E」或「e」的後面。

例如,「123.456E-5」、「0.000」、「E3」等是合法的,「123.4.56E5」、「-1233E0.2」、「2E」是不合法的。str_to_st0 有一大部份在檢查輸入的字串是否合法,以及化簡,這部份小木偶自認寫得不好,似乎是硬幹,期待有較好的演算法能檢測字串是否合法及化簡。小木偶的想法很簡單,把輸入的字串,分成係數及十的冪方數兩部份。以「E」或「e」作為界線,在「E」或「e」之前為係數,存於「str_manti」區域變數堙F在「E」或「e」之後為十的冪方,存於「str_exp」區域變數堙C簡化後的「str_manti」、「str_exp」兩字串,變成是以 ASCII 字元堛漯拉伯數字及小數點所形成之字串。例如

    pStr所指字串   sign_manti  str_manti   sign_exp    str_exp
    0.00200            0       0.002          0           0
    00254.125E-02      0       254.125        1           2
    e+08               0       1              0           8
    -e-5               1       1              1           5
    -1.602E-19         1       1.602          1           19

為何要這樣分開呢?因為從一個字串計算出整數,很簡單,而且不會有誤差。這件事交由副程式 calc_integer 去做,其原理很簡單。舉例來說,一個字串「245」代表三位數,可以看成是三次的迴圈。RAX 要記錄最後的數值,先使 RAX 為 0。每次迴圈先使 RAX 乘以 10,再讀取字串的一位數,因此做完第一次迴圈,RAX 為 2。第二次迴圈時,10×2+4,RAX 變成 24。做完第三次迴圈時,10×24+5,RAX 就變成 245 了。如果把係數與十的冪方分開,後者必為整數,很容易就能算出來;而係數也可以分成整數部分與小數部分。整數部分也可以通過 calc_integer 計算出來;而小數部分其實也可以想程式某個整數,除以 10 的某個次方,例如,0.125 可以看成是 125 除以 103,而這「某個次方」的數值,恰好是小數點後的位數。以 254.125×10-2 為例子,這個數可以寫成

254.125×10-2=(254+125/1000)×10-2=254×10-2+125×10-2-3

換句話說,現在已經把問題變化成:把兩個字串「245」、「125」變成數值,再去計算 10-2 及 10-5 然後把這四個數值,放入 FPU 媢B算,就能得到答案了。


原始碼:two_p_x、x_p_y、st0_to_sn、str_to_st0

底下是 two_p_x、x_p_y、st0_to_sn、str_to_st0 四個副程式的原始碼,小木偶把他們寫成動態連結程式庫的形式,命名為 MYFPU.ASM

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
;此程式包含四個副程式,均為64位元版本
;1.two_p_x:求2^X。
;2.x_p_y:求X^Y。
;3.st0_to_sn:把ST之數值,變成科學記號(scientific notation)形式,以阿拉伯數字的字串存於記
;       憶體內。
;4.str_to_st0:把字串轉換成暫時實數,存於ST
        OPTION  CASEMAP:NONE
        OPTION  WIN64:7
;*****************************************************************************************
.CODE
;-----------------------------------------------------------------------------------------
DLLEntry        PROC    hInstance,dwReason,dwReserved
                mov     rax,1
                ret
DLLEntry        ENDP
;-----------------------------------------------------------------------------------------
;目的:求2^x,此指數可以是整數、負數、浮點數。此副程式用到堆疊暫存器深度為ST(3)。
;輸入-ST(0):指數
;輸出-ST(0):2的次方數,原來存於ST(0)的指數會被破壞
;備註:1.此副程式可以用在pentium及其以上等級的FPU。
;      2.此副程式原理是利用2^(a+b)=(2^a)*(2^b),a 表示整數部分,b表示小數部分
two_p_x         PROC
                LOCAL   cw:WORD
                fstcw   cw       ;取得控制字組
                fwait            ;等待 pentium 儲存完畢
                push    cw       ;保存原控制字組
                and     cw,0f3ffh;使控制字組變成向負無窮大捨入,欲達此目
                or      cw,00400h;的必須使控制字組第10、11位元變為01
                fldcw   cw       ;載入新的控制字組
                fld     st       ;   x   ;   x   ;
                frndint          ;i=int x;   x   ;向負無窮大捨入
                pop     cw                       ;取回舊的控制字組
                fldcw   cw       ;   i   ;   x   ;載入舊的控制字組
                fsub    st(1),st ;   i   ; f=x-i ;ST(1)為小數部分,f
                fxch             ;   f   ;   i   ;交換
                f2xm1            ; 2^f-1 ;   i   ;求2的小數部分次方
                fld1             ;   1   ; 2^f-1 ;   i  ;載入1
                faddp   st(1),st ;  2^f  ;   i   ;      ;完成2的小數部分次方
                fscale           ;  2^x  ;   i   ;      ;計算ST(0)*2^ST(1)
                fstp    st(1)    ;  2^x  ;       ;      ;去掉整數部分
                ret
two_p_x         ENDP
;-----------------------------------------------------------------------------------------
;x_p_y副程式用來計算X^Y,X=ST(0),Y=ST(1),返回時將X^Y存於ST(1)後,再把堆疊彈出一次。
;x_p_y儘量算出X^Y,如下表
;X\Y 正    零   負
;正  X^Y    1   X^Y
;零  0   不定  無意義
;負  ☆    1   ☆
;☆:若Y為整數,才能求X^Y之值;若Y含有小數部份,X^Y=無意義
;輸入:指數(Y)-ST(1)
;   底數(X)-ST
;輸出:CY-如果X^Y無意義或未定值,則傳回CY
;   NC-計算成功,X^Y存於ST
x_p_y           PROC    USES rax
                LOCAL   power:DWORD
                LOCAL   cw:WORD ;控制字組
;檢查底數是否為零,如果是,跳至base_0;如果小於0,跳至base_ng;如果大於零,跳到calc處。
                ftst
                fstsw   ax      ;把狀態字組存入AX
                sahf            ;把狀態字組的高位元部份移入旗標
                jz      base_0  ;若ZR,底數為0,跳至base_0;
                jc      base_ng ;--st0--;--st1--;若CY,表示底數為負數,跳至base_ng:
                                ;   X   ;   Y   ;
calc:           fyl2x           ; Ylog2X;
                call    two_p_x ;  X^Y  ;
                jmp     finish
base_0:         fcomp           ;底數為零,彈出底數,FPU堆疊內只剩指數
                ftst            ;檢查指數是否為零
                fstsw   ax
                sahf
                jbe     err3    ;在底數為零的情形下,0^0=不定,0^負數=無意義
                fcomp           ;底數為零且指數為正,因為底數已彈出,此處彈出指數即可
                fldz            ;0^正數=0
                jmp     finish
;若底數為負值,則檢查指數是否為0、非零整數、含有小數。(X=底數,Y=指數)
;如果指數為0,X^0=1;如果指數為非零整數,可計算X^Y;如果指數含有小數部份,則無法計算X^Y
base_ng:        fstcw   cw      ;取得控制字組
                and     cw,0f3ffh;使控制字組變成向負無窮大捨入,欲達此目
                or      cw,00400h;的必須使控制字組第10、11位元變為01
                fldcw   cw      ;--st0--;--st1--;--st2--;
                fld     st(1)   ;   Y   ;   X   ;   Y   ;
                ftst            ;檢查指數是否為0
                fstsw   ax      ;把狀態字組存入AX
                sahf            ;把狀態字組的高位元部份移入旗標
                jz      exp_z   ;如果指數為0,跳至exp_z處
                frndint         ;i=INT Y;   X   ;   Y   ;
                fist    power   ;   i   ;   X   ;   Y   ;
                fsub    st,st(2);   i   ;   X   ;   Y   ;
                ftst            ;檢查ST是否為0,若為0,表示指數為整數;否則指數含有小數
                fstsw   ax
                sahf            ;把狀態字組的高位元部份移入旗標
                jnz     err2    ;若ST≠0,表示指數不為整數,跳至err2處
                fcomp           ;   X   ;   Y   ;       ;若ST=0,表指數為整數且底數為負數
                test    power,1 ;測試指數為奇數或偶數
                jnz     exp_odd
                fchs            ;  -X   ;   Y   ;       ;指數為偶數,底數為負值,但X^Y之值為正
                jmp     calc
exp_odd:        fchs            ;  -X   ;   Y   ;
                fyl2x           ; Ylog2X;
                call    two_p_x ;  X^Y  ;
                fchs
                jmp     finish

;底數為負值,指數為0,X^0=1。X=底數
exp_z:          fcompp
                fcomp
                fld1
finish:         clc
                jmp     exit

err2:           fcompp
err3:           fcomp           ;指數與底數均為零或底數為負數
                stc             ;設定進位旗標
exit:           ret
x_p_y           ENDP
;-----------------------------------------------------------------------------------------
;st0_to_sn把ST內的暫時實數,變成科學記號(scientific notation),存於pBuffer所指的記憶體內。
;flag:第0∼4位元:為係數有幾位數(包含個位數及小數點後的位數,例如1.234有4位),最多16位,
;         不可為0
;   第8個位元:如果為0,表示十的冪方數為正號時,不儲存「+」;如果為1,表示十的冪方數為
;         正號時,要儲存「+」
;   第9個位元:若為0,表示係數為正號時,不儲存「+」;若為1,表示係數為正號時,要儲存「+」
;   第10個位元:如果為0,表示pBuffer所指的記憶體,以ASCII碼儲存;如果是1,則以萬國碼儲存
;         如果flag指定ASCII碼:此記憶體至少要有27個位元組。前21個位元組放置係數,後
;                   6個位元組放置10的冪方數(exponential)。因此第0∼20個位
;                   元組用於存放係數;第21∼26個位元組用於存放十的冪方。
;         如果flag指定萬國碼:此記憶體至少要有54個位元組。前42個位元組放置係數,後
;                   12個位元組放置10的冪方數(exponential)。因此第0∼41個
;                   位元組用於存放係數;第42∼53個位元組用於存放十的冪方。
;pBuffer為一位址,指向一塊記憶體,長度至少為27或54個位元組(前者為ASCII,後者為萬國碼)。所存
;      放資料,分兩部份:
;  1.係數:大小應為21個(或42)位元組,有可能使用時比21個(或42)位元組少,它用來儲存係數(亦
;      稱尾數mantissa),分三部份:
;     a.第0個位元組(或第0個字組)為符號(+或-),如果flag中指定正號不儲存,此位元組就是係
;      數的整數部份。
;     b.其次的19個位元組(或19個字組)為係數(此19個位元組包含「.」或19個字組),若flag指
;      定不須這麼多位的係數,就可能會少於19個位元組(或19個字組)。如果flag的0∼4位元指
;      定的位數多於實際係數的位數時,則小數點後的0不會顯示出來,例如flag=5,ST=123
;      ,那麼只會顯示1.23E2,不會顯示1.2300E2。
;     c.最後一個位元組(或字組)是「0」,緊接著b.之後。
;  2.十的冪方:大小應為6個位元組(或6個字組),用來儲存10的次方數。第0個位元組(或第0個字組)
;      為10的次方數的符號(+或-),接下來的4個位元組(或4個字組)為指數部份,最後一個位元
;      組(或字組)為「0」
;輸入:ST-要轉換的暫時實數,但執行完後,ST之值不變
;   pBuffer-轉換成以阿拉伯數字為形式的科學記號存放位址
;   flag-旗標,見上面說明
;輸出:NC-如果轉換成功,NC,且pBuffer所指字串有科學記號;CY-錯誤,有可能是flag為0
st0_to_sn       PROC    USES rax rcx rdx rsi rdi pBuffer:QWORD,flag:QWORD
                LOCAL   cw:WORD ;控制字組
                LOCAL   power:WORD
                LOCAL   unicode:WORD    ;19=ASCII字元;38=萬國碼
                LOCAL   len_manti:QWORD ;呼叫者指定的係數位數,亦即flag中0∼4位元的數值
                LOCAL   mantissa:TBYTE
                LOCAL   exp:TBYTE
                LOCAL   x87stat[108]:BYTE
                fsave   x87stat
                lea     rax,x87stat
                mov     rdx,flag
                fld     TBYTE PTR [rax+28]
                and     rdx,1fh ;若呼叫者指定flag的0∼4位元為0,跳出st0_to_sn並設CY
                jnz     n_zero0 ;若呼叫者指定flag的0∼4位元不為0,則DX=呼叫者指定的係數位數
error:          stc
                jmp     exit1
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;底下為一副程式,依flag指定存入ASCII或萬國碼,在rdi所指位址存入位元組或字組
st_hi_nibble:   mov     al,[rsi]
                shr     al,4
store_number:   add     al,"0"
store:  .IF unicode==19
                stosb
        .ELSE
                stosw
        .ENDIF
                DB      0c3h    ;0c3h=ret的機械碼。如用ret,還會被組譯器多加leave等指令
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;底下的副程式,把一位元組的較低四位元變成阿拉伯數字,存於rdi所指記憶體
st_low_nibble:  mov     al,[rsi]
                and     al,0fh
                call    store_number
                DB      0c3h
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;處理係數與指數的符號。如果rcx=0,表示即使正數,也要儲存「+」;如果rcx=1,不儲存「+」
sign:   .IF BYTE PTR [rsi+9]==80h
                mov     al,"-"
sv_pos:         call    store
        .ELSE
                mov     al,"+"
                jrcxz   sv_pos
        .ENDIF
                DB      0c3h
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
word_ten        DW      10
szNanW          DW      975eh,6578h,503ch,0h            ;非數值
szPosInfinityW  DW      6b63h,7121h,7aaeh,5927h,0h      ;正無窮大
szNegInfinityW  DW      8ca0h,7121h,7aaeh,5927h,0h      ;負無窮大
szError         DW      45h,52h,52h,4fh,52h,0h          ;ERROR
szNanA          DB      "非數值",0,0
szPosInfinityA  DB      "正無窮大",0,0
szNegInfinityA  DB      "負無窮大",0,0
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
n_zero0:        test    flag,400h
        .IF ZERO?
                mov     unicode,19      ;19代表存入ASCII字元
        .ELSE
                mov     unicode,38      ;38代表存入萬國碼
        .ENDIF
;如果呼叫者指定flag的係數位數超過16位,以16位計算,存於len_manti及rdx
        .IF rdx>16
                mov     rdx,10h
        .ENDIF
                mov     len_manti,rdx
                mov     rdi,pBuffer
;檢查ST是否為不支援、非數值、無窮大、空的、反常值、0,如果是前五者,則把對應的字串寫入
;pBuffer所指的位址,設定CY後返回;如果是0,跳到zero1:標記處;如果是數值,則跳到value:處
                fxam
                add     rsi,9   ;指向係數的符號
                fstsw   ax
                xor     rcx,rcx
                bt      rax,8   ;把AH的第0、2、6三個位元,變成CL的第0、1、2位元
                jnc     @f      ;AH的這三個位元,分別對應C0、C2、C3
                or      cl,1
@@:             bt      rax,10
                jnc     @f
                or      cl,2
@@:             bt      rax,14
                jnc     @f
                or      cl,4
@@: .IF cl==2
                jmp     value
    .ELSEIF cl==4
                jmp     zero1
    .ELSEIF cl==1
                lea     rsi,szNanW
                mov     rcx,8
        .IF unicode==19
                lea     rsi,szNanA
        .ENDIF
    .ELSEIF cl==3
                mov     rcx,10
                test    ah,2
        .IF ZERO?
                lea     rsi,szPosInfinityW
            .IF unicode==19
                lea     rsi,szPosInfinityA
            .ENDIF
        .ELSE
                lea     rsi,szNegInfinityW
            .IF unicode==19
                lea     rsi,szNegInfinityA
            .ENDIF
        .ENDIF
    .ELSE
        .IF unicode==19
                lea     rsi,szErrorA
                mov     rcx,6
        .ELSE
                lea     rsi,szErrorW
                mov     rcx,12
        .ENDIF
    .ENDIF
                rep     movsb
                jmp     error
;若ST=0,把係數及十的冪方數皆填入30h、0,然後跳至exit0處。
zero1:          mov     rax,30h
                call    store
                movzx   rax,unicode
                add     rdi,rax
                xor     rax,rax
                stosw
                jmp     exit0
;若ST不為零,先計算出指數為何   ;--st0--;--st1--;--st2--;
value:          fld     st      ;   x   ;   x   ;       ;假設要印出的數為x
                fabs            ;  |x|  ;   x   ;
                fld1            ;   1   ;  |x|  ;   x   ;
                fxch    st(1)   ;  |x|  ;   1   ;   x   ;
                fyl2x           ;log2|x|;   x   ;
                fldl2t          ;log2 10;log2|x|;   x   ;
                fdivp   st(1),st;e=log x;   x   ;指數為「e的整數部份」
                fstcw   cw                      ;取得現有捨入方法,變為「向負無窮大」捨入
                and     cw,not 0c00h
                or      cw,0400h
                fldcw   cw                      ;載入「向負無窮大」捨入
                frndint         ;i=int e;   x   ;把e捨入成i,i即為指數部份
                fist    power   ;   i   ;   x   ;把指數存於power變數
                fbstp   exp     ;   x   ;       ;把i變成聚集BCD,存入exp變數
                and     cw,0f3ffh               ;改成四捨五入法,處理係數
                sub     dx,power
                dec     dx
                lea     rsi,mantissa
                fldcw   cw
                mov     power,dx;DX=len_manti-i-1  ;原數×10^DX,使係數在整數位四捨五入
                fild    power   ;   p   ;   x   ;       ;p=DX
                fild    word_ten;   10  ;   p   ;   x   ;
                call    x_p_y   ;  10^p ;   x   ;
                fmul    st,st(1); x*10^p;   x   ;
                frndint         ; x*10^p;   x   ;       ;四捨五入到呼叫者指定位數
                fbstp   mantissa
;處理係數的符號,rsi+9指向mantissa的第九個位元組
                xor     rcx,rcx
                mov     rax,rcx
                test    flag,200h
                jnz     manti_sign
                inc     rcx     ;若rcx=1,表示即使係數為正,也不儲存「+」
manti_sign:     call    sign
;計算係數的整數位數由哪一位元組開始。呼叫者指定的係數位數,每兩位構成一個位元組,故係數位
;為1、2,不使rsi加一;係數位數3、4,rsi須加一;係數位數56,須加二;……
                mov     rdx,len_manti
                mov     rcx,len_manti
                dec     rdx
                shr     rdx,1
                add     rsi,rdx         ;rsi指向係數整數所在位址
                test    len_manti,1     ;若呼叫者指定的係數位數為奇數,則從某個位元組的低
                lea     rdx,mantissa    ;四位元開始;否則從高四位元開始
                jz      int_at_hi
                call    st_low_nibble
                dec     rcx
                jz      del_end_zero
                mov     al,"."
                call    store
                jmp     next_byte
int_at_hi:      call    st_hi_nibble
                dec     rcx
                jz      del_end_zero
                mov     al,"."
                call    store
                jmp     st_low
next_byte:      dec     rsi
                call    st_hi_nibble
st_low:         call    st_low_nibble
                cmp     rsi,rdx
                je      del_end_zero
                jmp     next_byte
;如果有效位數末幾位為0,去掉這些0
del_end_zero:   mov     rdx,-2
        .IF unicode==19
                mov     rdx,-1
        .ENDIF
@@:             cmp     BYTE PTR [rdi+rdx],"0"
                jne     @f
                add     rdi,rdx
                jmp     @b
@@:             cmp     BYTE PTR [rdi+rdx],"."  ;檢查是否小數點後全為0
                jne     @f
                add     rdi,rdx         ;如果小數點後全為0,去掉小數點
@@:             xor     rax,rax
                call    store

;底下的程式,開始處理十的冪方數。先使rsi、rdi分別指向exp及pBuffer所指指數位址
                mov     rdi,pBuffer
                lea     rsi,exp
        .IF unicode==19
                add     rdi,21
        .ELSE
                add     rdi,42
        .ENDIF
;檢查十的冪方是否為0,如果為0,直接存入阿拉伯數字「0」
                cmp     WORD PTR [rsi],0;因指數最多只有四位數,剛好一個字組
                jnz     non_zero3
                mov     rax,30h
                stosd
                jmp     exit0
non_zero3:      xor     rcx,rcx         ;處理十冪方的符號
                test    flag,100h       ;如果rcx=1,表示即使指數為正,也不儲存「+」
                jnz     exp_sign
                inc     rcx
exp_sign:       call    sign
                inc     rsi             ;因為FPU暫時實數範圍3.4E-4932∼1.2E4932
                mov     al,[rsi]        ;故只需處理兩個位元組的聚集BCD數即可
        .IF al==0
                movzx   rdx,al          ;rdx=0,表示高位元組為0,例如指數是10^0025,
                jmp     low_byte        ;前面兩個0,不必儲存,只需存入25即可
        .ELSE
                mov     rdx,1           ;rdx=1,表示高位組不為0,即使低位元組有0
                shr     al,4            ;也要儲存0
                jz      zero2
                call    store_number
zero2:          call    st_low_nibble
        .ENDIF
low_byte:       dec     rsi
                mov     al,[rsi]
        .IF rdx==0
                shr     al,4            ;若rdx=0,表示高位元組為0,如果AL的高四位元亦為
                jz      zero3           ;0,那麼此0不儲存;否則此0要儲存
                call    store_number
zero3:          call    st_low_nibble
        .ELSE
                shr     al,4
                call    store_number
                call    st_low_nibble
        .ENDIF
                xor     rax,rax
                call    store
exit0:          clc
exit1:          frstor  x87stat
                ret
st0_to_sn       ENDP
;-----------------------------------------------------------------------------------------
;str_to_st0能把數字字串,轉換成暫時實數(80位元長的實數),並儲存在ST(0)堙C
;輸入:pStr-數字字串位址,此數字字串以0結尾,例如38 31 32 2E 39 45 35 00,表示「812.9E5」
;      0,數字字串可以包含"0"∼"9"阿拉伯數字、小數點、正負號及"E"或"e"(表示十的冪方)
;      ,其餘字元都是不合法的。如果pStr所址字串為萬國碼,最大長度為54個位元組;如果
;      是ASCII碼,最大長度是27個位元組。pStr所指的字串,可以包含十的冪方,以"E"或"e"
;      後面的數表示,可以是正數,也可以是負數,但不能包含小數點。
;   unicode-此數字字串是否為萬國碼。1,萬國碼;0,ASCII碼
;輸出:CY-錯誤;NC-正常結束,ST存入暫時實數
str_to_st0      PROC    USES rax rbx rcx rdx rsi rdi pStr:QWORD,unicode:QWORD
;sign_manti=0,正數;sign_manti=1,負數。sign_exp=0,正數;sign_exp=1,負數
                LOCAL   sign_manti:BYTE,sign_exp:BYTE
                LOCAL   exp:WORD
                LOCAL   pPoint:QWORD    ;str_manti字串中小數點位址,若無小數點,pPoint=0
                LOCAL   pEndManti:QWORD ;str_manti字串最後一數字的位址,即結尾的NULL位址
                LOCAL   qword_integer:QWORD
                LOCAL   str_manti[20h]:BYTE
                LOCAL   str_exp[10h]:BYTE
                LOCAL   x87stat[108]:BYTE
                fsave   x87stat
                cld
                mov     al,0
                mov     rdx,26          ;rdx=呼叫者傳來pStr所指定字串的最大長度是幾位元組
                cmp     unicode,0       ;檢查unicode參數只能是0或1,如果不是,錯誤
                je      max_len_ok
                cmp     unicode,1
                je      uni_max_len
error:          stc
                jmp     exit1
uni_max_len:    mov     rdx,52
max_len_ok:     lea     rdi,str_exp     ;把str_exp字串設為0
                mov     rcx,SIZEOF str_exp
                rep     stosb
                lea     edi,str_manti   ;;把sti_manti字串設為0
                mov     rcx,SIZEOF str_manti
                rep     stosb
                mov     sign_manti,al
                mov     sign_exp,al
;先檢查是否有不合法字元,除了阿拉伯數字、小數點、「E」、「e」、正負號以外,都是不合法的。
;除了檢查之外,也把每個阿拉伯數字變成一個位元組的ASCII字元,存到str_manti、str_exp字串
;CL代表小數點個數,CH代表"E"的個數,CL、CH只能是1或0。因為前面已執行過rep stosb,故rcx=0
;BL表示是否儲存"0",要儲存BL=1,如小數點後的0、整數已出現非零數字後的0;不儲存,BL=0
;,如00022,前三個0不儲存
                mov     rsi,pStr
                lea     rdi,str_manti
                add     rdx,rsi ;rdx=pStr最後位址
                mov     rbx,rcx
    ;如果第一個字元是正負號、"E"或0,特別處理。
    ;正負號儲存到sign_manti變數堙A再檢查第二個字元是否為"E"或"e",如果是的話,在
    ;str_manti儲存一個位元組"1",此外,如果是萬國碼,高位元組應為0,否則錯誤。
    .IF BYTE PTR [rsi]=="+"
ascii_or_uni:   cmp     unicode,1
                jne     ascii_str
                cmp     BYTE PTR [rsi+1],0
                jne     error
                cmp     WORD PTR [rsi+2],0      ;正負號後為0
                je      error
                cmp     WORD PTR [rsi+2],"E"
                je      deal_e0
                cmp     WORD PTR [rsi+2],"e"
                jne     next_char_addr
deal_e0:        add     rsi,2
deal_e1:        mov     al,"1"
                stosb
                jmp     deal_e3
ascii_str:      cmp     BYTE PTR [rsi+1],0      ;正負號後為0
                je      error
                cmp     BYTE PTR [rsi+1],"E"
                je      deal_e2
                cmp     BYTE PTR [rsi+1],"e"
                jne     next_char_addr
deal_e2:        inc     rsi
                jmp     deal_e1
    .ELSEIF BYTE PTR [rsi]=="-"
                inc     sign_manti
                jmp     ascii_or_uni
    .ELSEIF (BYTE PTR [rsi]=="E")||(BYTE PTR [rsi]=="e")
                jmp     deal_e1 ;呼叫者輸入類似"E2"(即100)的字串
    .ELSEIF BYTE PTR [rsi]==0   ;呼叫者輸入空字串
                jmp     error
    .ENDIF
    ;檢查每個字元是否在規定的範圍內,且把係數放在str_manti字串,指數放在str_exp字串。
    ;其符號各存放在sign_manti、sign_exp堙A並除去多餘的0。範例如下表:
    ;呼叫者         sign_manti  str_manti   sign_exp    str_exp
    ;0.00200        0           0.002       0           0
    ;00254.25E-02   0           254.25      1           2
    ;e+08           0           1           0           8
    ;-e-5           1           1           1           5
    ;-1.602E-19     1           1.602       1           19
next_char:      mov     al,[rsi]
    .IF al==0
        .IF pEndManti==0                ;如果已到pStr所指字串結尾,pEndManti仍為0
                mov     pEndManti,rdi   ;表示pStr所指字串中,無"E"或"e"字元
        .ENDIF
                jmp     calc_exp
    .ELSEIF al=="."
                inc     cl
                cmp     cl,1            ;超過一個小數點
                ja      error
                cmp     bl,0            ;若BL=0,表示類似「000.12」無整數部份
                jnz     @f              ;故要存一個"0",再存一個"."
                mov     BYTE PTR [rdi],"0"
                inc     rdi
@@:             mov     pPoint,rdi
                mov     bl,1
                stosb
    .ELSEIF al=="0"
                cmp     bl,0
                jz      next_char_addr
                stosb
    .ELSEIF (al>="1")&&(al<="9")
                mov     bl,1            ;出現非零數之後的"0",要儲存
                stosb
    .ELSEIF (al=="E")||(al=="e")
deal_e3:        inc     ch
                cmp     ch,1            ;超過一個"E"或"e"
                ja      error
                mov     pEndManti,rdi
                mov     bl,0            ;出現"E"後,在非零數之前的"0",不儲存
                lea     rdi,str_exp     ;若"E"或"e",使rdi=str_exp字串位址,以存十的冪方
        .IF unicode==1
                add     rsi,2
                cmp     BYTE PTR [rsi],0;"E"後面沒有字元,錯誤
                je      error
                cmp     WORD PTR [rsi],"+"
                jne     chk_neg0
                cmp     WORD PTR [rsi+2],0
                je      error
                jmp     next_char_addr
chk_neg0:       cmp     WORD PTR [rsi],"-"
                jne     next_address
                cmp     WORD PTR [rsi+2],0
                je      error           ;"E-"之後沒有字元,錯誤
                inc     sign_exp
        .ELSE
                inc     rsi
                cmp     BYTE PTR [rsi],0
                je      error
                cmp     BYTE PTR [rsi],"+"
                jne     chk_neg1
                cmp     BYTE PTR [rsi+1],0
                je      error           ;"E+"之後沒有字元,錯誤
                jmp     next_char_addr
chk_neg1:       cmp     BYTE PTR [rsi],"-"
                jne     next_address
                cmp     BYTE PTR [rsi+1],0
                je      error           ;"E-"之後沒有字元,錯誤
                inc     sign_exp
        .ENDIF
    .ELSE
                jmp     error           ;出現不合法字元
    .ENDIF
next_char_addr: test    unicode,1       ;依據pStr字串是否為萬國碼,決定rsi加一或加二
                jz      not_unicode     ;如果是萬國碼,高位元組應為0,否則錯誤
                cmp     BYTE PTR [rsi+1],0
                jnz     error
                add     rsi,2
                jmp     next_address
not_unicode:    inc     rsi
next_address:   cmp     rsi,rdx ;如果pStr所指字串,超過長度還沒結束,錯誤
                je      error
                jmp     next_char
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
qword_ten       DQ      10
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;calc_integer副程式把rsi所指字串內的阿拉伯數字字串,變成十六進位整數,存於RAX
calc_integer:   xor     rax,rax
next_number:    movzx   rbx,BYTE PTR [rsi]
                cmp     bl,0
                je      calc_int_ok
                cmp     bl,"."
                je      calc_int_ok
                mul     qword_ten
                sub     bl,"0"
                add     rax,rbx
                jmp     next_number
calc_int_ok:    mov     qword_integer,rax
                DB      0c3h
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;計算十的冪方數,亦即把str_exp字串,變成十六進位數值,並存於exp變數堙Cstr_to_st0副程式執
;行到calc_exp處,已經把呼叫者傳來的字串,變成係數字串(str_manti)及冪方字串(str_exp),且這
;兩個字串,都已簡化。
calc_exp:       lea     rsi,str_exp
                call    calc_integer
                mov     exp,ax
;檢查呼叫者輸入的pStr所指字串,是否類似"0"、"000"等字串,此字串表示數值0
                mov     rsi,pEndManti
                lea     rdx,str_manti
                cmp     rsi,rdx
                jne     chk_end_0
zero_input:     fldz
                jmp     exit0
;檢查呼叫者輸入的pStr所指字串,是否類似"00.000"、"000.00E2"等字串,此字串也表示數值0。做此
;檢查時,先刪除小數點後,字串結尾的"0";但在刪除0之前,先檢查是否沒有小數部分
chk_end_0:      mov     rbx,pPoint      ;如果沒有小數點,表示沒有小數,只有整數
                or      rbx,rbx
                jz      del_end_0_ok
@@:             cmp     BYTE PTR [rsi-1],"0"
                jne     @f
                dec     rsi
                jmp     @b
@@:             cmp     BYTE PTR [rsi-1],"."
                jne     point_part
                dec     rsi
                mov     BYTE PTR [rsi],0
                mov     pEndManti,rsi
                mov     pPoint,0
                jmp     del_end_0_ok
point_part:     mov     BYTE PTR [rsi],0
                mov     pEndManti,rsi   ;--st0--;--st1--;--st2--;
;呼叫者所傳入的字串所代表的數值,可以寫成r,r=m×10^e,m可以寫成整數部分,i,及小數部分f
;。f又可以看成是小數部分乘以某個十的冪方,而成的最小整數再除以某個十的冪方。例如,呼叫者
;輸入的字串所代表的數為1.602×10^2,m=1.602,i=2,f=0.602=602÷1000=x÷10^3,x即為小數
;部分乘以某個十的冪方,而成的最小整數。某個十的冪方的冪方數即為10^(小數位數)
;r=(i+f)×10^e=i×10^e+f×10^e=i×10^e+x÷10^d×10^e,d=小數位數
del_end_0_ok:   fild    exp             ;   e   ;e=pStr所指字串中十的冪方絕對值
                cmp     sign_exp,1
                jne     pos_exp
                fchs                    ;如果十的冪方為負,則改變ST(0)之符號
                fist    exp             ;exp=真正的十的冪方數
pos_exp:        fild    qword_ten       ;  10   ;   e   ;       ;
                call    x_p_y           ; 10^e  ;       ;       ;
                lea     rsi,str_manti
                call    calc_integer
                fild    qword_integer   ;   i   ;  10^e ;       ;
                fmul                    ; i*10^e;       ;       ;
                cmp     pPoint,0        ;如果pPoint=0,表示沒有小數部分
                je      exit0
                mov     rbx,pEndManti
                sub     rbx,pPoint
                dec     rbx             ;rbx=d=pStr所指字串中,小數位數
                sub     exp,bx          ;exp=e-d=p
                fild    exp             ;   p   ; i*10^e;       ;
                fild    qword_ten       ;  10   ;   p   ; i*10^e;
                call    x_p_y           ; 10^p  ; i*10^e;       ;
                mov     rsi,pPoint
                inc     rsi
                call    calc_integer    ;底下的f=pStr所指字串的小數部分,變成的最小整數
                fild    qword_integer   ;   x   ; 10^p  ; i*10^e;
                fmul                    ; x*10^p; i*10^e;       ;
                fadd                    ;   r   ;       ;       ;
exit0:          cmp     sign_manti,1
                jne     pos_manti
                fchs
pos_manti:      clc
                fstp    TBYTE PTR str_manti
exit1:          frstor  x87stat
                jc      exit2
                fld     TBYTE PTR str_manti
exit2:          ret
str_to_st0      ENDP
;*****************************************************************************************
END             DLLEntry

模組定義檔,MYFPU.DEF 的內容如下:

1
2
3
4
EXPORTS
        two_p_x
        x_p_y
        st0_to_s

依底下的方式組譯及連結,就能得到 MYDLL.DLL 及 MYDLL.LIB:

E:\HomePage\SOURCE\Win64\FPU>uasm64 -win64 mydll.asm [Enter]
UASM v2.46, Jan  8 2018, Masm-compatible assembler.
Portions Copyright (c) 1992-2002 Sybase, Inc. All Rights Reserved.
Source code is available under the Sybase Open Watcom Public License.

MYDLL.ASM: 645 lines, 4 passes, 15 ms, 0 warnings, 0 errors

E:\HomePage\SOURCE\Win64\FPU>link /DLL /DEF:myfpu.def myfpu.obj [Enter]
Microsoft (R) Incremental Linker Version 9.00.21022.08
Copyright (C) Microsoft Corporation.  All rights reserved.

/SUBSYSTEM:WINDOWS /DEBUG
   Creating library myfpu.lib and object myfpu.exp

E:\HomePage\SOURCE\Win64\FPU>

在副程式堶悸漱煽O副程式

在 st0_to_sn 副程式堙A常會用到要把阿拉伯數字,依據呼叫者所指定的方式,儲存成 ASCII 或萬國碼字元。如果把它寫成副程式,就能節省程式碼。想要在副程式堙A要安插個副程式,並不是常規的做法,但是也不是不可以。這種副程式是直接將其程式碼,安插在另一個副程式中,似乎沒有特別的名稱,小木偶姑且稱為內嵌副程式。在 st0_to_sn 副程式堙A其實就安插了三個內嵌副程式,在第 165∼192 行,小木偶以一整行的「;;;;;;;……」分隔。

您可以檢視這個例子,您會發現內嵌副程式,是以標號代替副程式名稱,而在此副程式結尾之處,以「DB 0c3h」代替「ret」指令即可。要呼叫內嵌副程式時,只需下達「call」指令即可。想當然耳,如果有參數的話,用堆疊傳遞可能較為麻煩,用變數或暫存器傳遞,可能是較方便的做法。

底下解釋為何不用「ret」指令返回副程式呢?這是因為組譯器在宣告副程式,和結束副程式時,會加上許多處理堆疊框的指令。當組譯器組譯原始碼時,看見「ret」指令時,就會知道副程式要結束了,就會加入一些處理堆疊框的指令。因此,如果原始碼中,內嵌副程式以「ret」返回,那麼組譯器會多加處理堆疊框的的指令。而在執行時,當內嵌副程式執行完成,要返回副程式時,如果執行到這些多出來處理堆疊框的指令,那麼就會返回到呼叫副程式的程式,而不是返回副程式了。所以內嵌副程式的返回指令,不能在原始碼中寫「ret」,但是實際上是,只要執行「ret」指令,卻不要有多出來處理堆疊框的指令。又因為「ret」指令的機械碼是「0c3h」,所以乾脆在原始碼寫入此位元組,就只得「ret」指令,而沒有多餘處理堆疊框的指令。如果想要在一副程式內,插入一內嵌副程式,其程式碼如下:

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
;-------------------------------------------------------------------------------
副程式名        PROC    USES 暫存器列表 參數1:DWORD,參數2:DWORD,...
                副程式之程式碼
                   .
                   .
                   .
                jmp     label1
;;;;內嵌之副程式開始;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
subroutine:     內嵌副程式之程式碼開始
                   .
                   .
                   .
                內嵌副程式之程式碼結束
DB              0c3h
;;;;內嵌之副程式結束;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
label1:         副程式其餘程式碼
                   .
                   .
                   .
                call    subroutine      ;呼叫內嵌之副程式
                   .
                   .
                   .
                副程式結束
                ret
副程式名        ENDP
;-------------------------------------------------------------------------------

另外,您也可以看見,如果在副程式中,要用到只有此副程式會使用到的常數,也可以在某次跳躍後,定義此常數。例如 193 行的 word_ten 常數,以及後面的常數字串,都是屬於此種情形。


後記

在撰寫 str_to_st0 的過程中,小木偶發現 UASM 組譯時,區域變數填入堆疊的位置,並沒有照順序,


閱讀附錄二