Monday, October 26, 2009

Lỗi Runtime error 200 trong Turbo Pascal 7.0

Như một số bạn sử dụng Borland Pascal 7.0 đã nhận xét trong số báo PCWorld tháng 3 năm 1999, với các bộ xử lý Intel Pentium Celeron, một số chương trình Pascal sau khi dịch và cho thực thi thì nhận được thông báo lỗi: Error 200: division by zero. Nếu đem đúng chương trình source cho dịch lại với Turbo Pascal 5.5 thì lại không có vấn đề gì xảy ra.

Điều khá đau lòng cho chúng ta là Borland, công ty tạo ra trình biên dịch nổi tiếng Turbo Pascal, sau một thời gian hoạt động thua lỗ đã quyết định đổi tên thành Inprise Inc. và bỏ rơi một số phần mềm của mình trong đó có Turbo Pascal. Version cuối cùng của Pascal do Borland đưa ra là 7.0. Việc cập nhật lên version mới hơn là điều không thực hiện được.

Là sinh viên của giai đoạn đầu thập kỷ 90, tôi đã khá viết khá nhiều chương trình bằng Turbo Pascal. Cách đây vài tháng, khi nâng cấp máy tính của mình lên Pentium Celeron 333, tôi đã nhận được thông báo lỗi tương tự như trên. Với một số bạn, nếu Turbo Pascal từ chối chạy với Celeron, viết lại chương trình bằng một ngôn ngữ khác có lẽ cũng chẳng thành vấn đề. Tuy nhiên, trong trường hợp của tôi, thời gian để dịch số source đã viết sang một ngôn ngữ khác như C hay C++ sẽ rất lớn. Chính vì vậy, tôi quyết định tìm hiểu nguyên nhân và cách khắc phục vấn đề trên. Trong bài viết này, xin chia sẻ kinh nghiệm của tôi cùng các bạn. Nếu không quan tâm tới nguyên nhân xâu xa của thông báo lỗi trên, bạn có thể bỏ qua mục Nguyên Nhân và đi thẳng tới phần Cách giải quyết.

Trái lại, nếu là người yêu thích ngôn ngữ Pascal và muốn đi sâu vào ngôn ngữ này, xin bỏ một chút thời gian cho mục Nguyên nhân, nơi mọi nguyên căn của vấn đề trên được trình bày.

Nguyên nhân

Trước hết, để có thể tìm được vị trí gây nên lỗi, chúng ta hãy dùng đoạn chương trình đơn giản sau:

Program Test;
Uses Crt;
Begin
Writeln('Hello');
End.

Rõ ràng, chương trình này chỉ viết chữ Hello lên màn hình. Khi các bạn chạy chương trình sẽ nhận được thông báo lỗi: Runtime error 200 at XXXX:0091. XXXX là địa chỉ segment gây lỗi, có thể khác với mỗi máy tùy theo các chương trình đã nạp trong bộ nhớ. 0091 là offset của đoạn mã lệnh gây nên lỗi.

Nếu bạn bỏ dòng thứ 2: Uses Crt thì chương trình lại chạy một cách "ngon lành". Sở dĩ chúng ta có thể bỏ dòng này là vì hàm writeln duy nhất dùng trong chương trình vừa thuộc unit SYSTEM ngầm định, vừa thuộc unit CRT. Trong trường hợp dùng lệnh Uses CRT, CRT.Writeln sẽ được gọi. Trái lại, SYSTEM.Writeln sẽ được sử dụng nếu bạn không viết Uses CRT. Vậy, lỗi có liên quan tới unit CRT.

Giữ nguyên dòng Uses CRT, và thêm dòng:

SYSTEM.writeln('System Hello')

vào trước dòng writeln('Hello'). Khi chạy chương trình, bạn cũng sẽ chẳng thấy dòng System Hello hiện ra, mà hệ thống vẫn đưa ra câu báo lỗi tương tự. Như vậy, phần mã gây lỗi nằm trước mã lệnh SYSTEM.Writeln(System Hello).

Từ 2 nhận xét trên, có thể phán đoán ngay phần mã gây lỗi là phần khởi động của unit CRT-phần được thực hiện đầu tiên khi chương trình Test được nạp vào bộ nhớ.

Nếu bạn dịch chương trình Test ra file EXE rồi tiến hành thi hành ngoài DOS thì sẽ nhận được thêm một chút thông tin về địa chỉ gây lỗi: Runtime error 200 at XXXX:0091. Error 200 theo Borland Pascal Help là lỗi Divide by zero (chia cho 0). Chương trình của chúng ta có cộng trừ nhân chia gì đâu mà gây nên lỗi này?

Như các bạn cũng biết, nếu dùng một unit nào đó, khi bắt đầu chạy chương trình, phần mã khởi động của Unit sẽ được thực hiện. Bằng cách sử dụng Turbo Debugger (tại dấu nhắc của Dos, nhập TD TEST), bạn có thể quan sát dễ dàng quan sát được các mã khởi động của unit SYSTEM và CRT vào đầu chương trình dưới dạng các lệnh Call Far:

CALL XXXX:0000 ; Khởi động unit system
CALL YYYY:000D ; Khởi động unit Crt

Trong Turbo Debugger, ấn F8 (Step Over) để thực hiện 1 lần lệnh thứ nhất và F7 (Trace) để bước vào chạy từng bước lệnh CALL thứ hai-lệnh khởi động CRT. Sau đó, chọn mục Run / Execute To, nhập offset địa chỉ cần tới là 0091 (hex), bấm Enter. Con trỏ sẽ hiện lên ở ngay trước vị trí đoạn mã lệnh gây lỗi( XXXX:0091). Nếu nhấn F8 1 lần nữa, Deburger sẽ hiện ra thông báo: Terminated, exit code 200. Để tiện theo dõi, tôi xin trích ra một phần các lệnh có liên quan phía trước và sau lệnh gây lỗi:

CRT_Initialize:
...............................
XXXX:0071: MOV ES,Seg0040
XXXX:0075: MOV DI,OFFSET Timer
XXXX:0078: MOV BL,ES:[DI] ; ES = 0040h, DI = 006Ch
XXXX:007b: @@2: CMP BL,ES:[DI] ; ES:DI = System clocktick
XXXX:007e:JE @@2
XXXX:0080: MOV BL,ES:[DI]
XXXX:0083: MOV AX,-28
XXXX:0086: CWD ; DX = FFFFh, AX = FFE4h
XXXX:0087: CALL DelayLoop
XXXX:008a: NOT AX
XXXX:008c: NOT DX
XXXX:008e: MOV CX,55
XXXX:0091: DIV CX ; Nỳi gờy lửợi
XXXX:0093: MOV DelayCount,AX
DelayLoop:
XXXX:02c6: @@1: SUB AX,1
XXXX:02c9: SBB DX,0
XXXX:02cc: JC @@2
XXXX:02ce: CMP BL,ES:[DI]
XXXX:02d1: JE @@1
XXXX:02d3: @@2: RET

Tôi không muốn bạn "sa lầy" vào hợp ngữ, xin giải thích ý chính của đoạn chương trình này như sau:

Khi phần khởi động của CRT chạy, chương trình sẽ khởi tạo một biến gọi là DelayCount (dạng word 2- byte). Biến này chứa số lần thực hiện các lệnh trong vòng lặp DelayLoop để làm chậm 1 mili giây. Nếu bạn cần delay N mili giây, hệ thống sẽ thực hiện N lần vòng DelayLoop.

Cách xác định DelayCount sẽ dựa vào giá trị xung đếm hệ thống (clocktick) tại địa chỉ 0040:006C (4 byte). Mỗi giây, nhờ kích hoạt của interrupt 08, giá trị tại địa chỉ này sẽ tăng lên 18.2 lần.

Trình biên dịch sẽ đặt vào DX:AX một giá trị cố định và gọi vòng lặp DelayLoop. Trong vòng lặp này, giá trị DX:AX sẽ được thay đổi cho tới khi clocktick của hệ thống thay đổi.

Đó là độ thay đổi của DX:AX trong 1 clocktick (1/18.2s). Để có giá trị này cho 1 mili giây, giá trị DX:AX sẽ được chia cho 55 (xin để ý: 18.2 * 55 = 1000ms=1second).

Đoạn lệnh gây lỗi trong chương trình chính là đoạn chia DX:AX cho CX. Về mặt nguyên tắc, thương số sẽ được chứa trong AX và số dư trong DX. Tuy nhiên, do DX:AX quá lớn, giá trị DX:AX chia cho CX sẽ vượt qua giá trị tối đa của 1 word (FFFFh) và hệ thống phát sinh thông báo lỗi.

Với máy Celeron 333 của tôi, giá trị nhận được khi chạy tới lệnh tại địa chỉ xxxx:008E là DX= 0045h, AX=4EEAh. Do đó, phép chia DX:AX (00454EEAh) cho CX(55=37h) sẽ làm tràn số và gây ra lỗi runtime. Nguyên nhân của vấn đề lỗi rõ ràng có liên quan tới tốc độ CPU. Do CPU chạy quá nhanh, độ biến đổi DX:AX là rất lớn và phép chia DX:AX cho CX bị tràn. CPU trong tầm 200Aạ266MHz có lẽ nằm ở ngưỡng của "vực thẳm" vì DX = 045h (giá trị ứng với Celeron 333MHz) * 266/333 = 37h = 55 (Phép chia DX:AX cho CX sẽ cho ra một giá trị xấp xỉ FFFFh. Do đó, với các hệ thống 200 - 266 MHz nhanh, lỗi trên có thể xuất hiện và với các hệ thống chậm, các bạn có thể chạy Turbo Pascal một cách yên ả). Như bạn có thể thấy, CPU tốc độ 300MHz, 333MHz hoặc cao hơn sẽ gây lỗi (do giá trị DX lớn hơn nhiều so với CX, dẫn tới DX:AX chia CX bị tràn). Như vậy, cách giải quyết của chúng ta sẽ xoay quanh việc làm giảm độ biến đổi DX:AX xuống. Điều này có thể thực hiện bằng cách làm chậm vòng DelayLoop xuống. Bạn có thể thêm 1 vài lệnh làm tiêu tốn CPU clock vào vòng lặp này như tôi thực hiện ở phần sau. Mọi thông báo sẽ chấm dứt và chương trình Pascal của bạn sẽ chạy một cách êm ả.

Cách giải quyết

  • Mọi vấn đề "đau đầu" nói trên sẽ được giải quyết bằng cách thay đổi nội dung tập tin CRT.TPU. Thông thường, khi bạn khởi động TURBO PASCAL (TP) hay BORLAND PASCAL (BP), CRT.TPU sẽ được hệ thống tự động nạp khi đọc TURBO.TPL (TPL=Turbo Pascal Library). Nhiệm vụ của chúng ta là thay đổi unit CRT trong TURBO.TPL.
  • Đơn giản nhất, bạn có thể chép tập tin TURBO.TPL từ tòa soạn về và ghi đè lên tập tin TURBO.TPL trong thư mục \BP\BIN (xem như \BP là thư mục Pascal của bạn). Tập tin TURBO.TPL mới chứa mọi thay đổi cần thiết giúp bạn có thể chạy BORLAND PASCAL với bộ xử lý Intel Pentium Celeron (và hy vọng mọi bộ xử lý khác "có vấn đề").
  • Nếu bạn không thể ghé tòa soạn và có thể truy xuất Internet hay e-mail, xin gửi e-mail cho namthang72@hotmail.com,, tôi sẽ gửi file TURBO.TPL lại cho bạn theo dạng attachment.
  • Và cuối cùng, nếu bạn không thể chép tập tin từ tòa soạn và cũng không có điều kiện sử dụng Internet, nếu cho mình là một lập trình viên Pro, hãy làm như sau:

Trước khi tiến hành các bước sau, hãy lưu lại tập tin TURBO.TPL trong \BP\BIN để đề phòng mọi bất trắc.

Bước 1: Sửa lại nội dung unit CRT

Với một bản cài đầy đủ của Borland Pascal 7.0, bạn sẽ có thư mục \BP\CRT, trong đó có 2 tập tin CRT.ASM và CRT.PAS. Đầu tiên, hãy thay đổi nội dung CRT.ASM như sau (hãy bấm Ctrl Q-L và tìm từ DelayLoop và phần mã sau):

; Delay one timer tick or by CX iterationsDelayLoop:
@@1:
SUB AX,1
SBB DX,0
JC @@2
CMP BL,ES:[DI]
JE @@1
@@2: RET

trở thành:

; Delay one timer tick or by CX iterations
DelayLoop:
@@1:
push ax ; bắt đầu các hàng chèn thêm
push cx push dx
mov ax,0
mov cx,1
mov dx,0
div cx ;với 10 lệnh div cx, DelayCount = 5F0h div cx; đủ cho bạn chạy với CPU có tốc độ <>
div cx ; về mặt lý thuyết.
div cx
div cx
div cx
div cx
div cx
div cx
div cx
pop dx
pop cx
pop ax ; kết thúc các hàng chèn thêm
SUB AX,1
SBB DX,0
JC @@2
CMP BL,ES:[DI]
JE @@1
@@2: RET

Như tôi có trình bày ở phần trên, các dòng chèn thêm không thực hiện bất cứ một lệnh gì mà chỉ đơn thuần là làm chậm vòng DelayLoop lại. Điều này không có nghĩa là đồng hồ hệ thống sẽ chậm lại mà chỉ đơn thuần làm chậm vòng DelayLoop và giảm số lần lặp xuống.

Lệnh nguyên thủy SUB và SBB của vòng lặp được giữ nguyên, các lệnh DIV (sau khi lưu nội dung thanh ghi AX, CX, DX bằng loạt lệnh PUSH) nhằm làm chậm hơn vòng lặp. Sở dĩ tôi chọn các lệnh này là vì DIV là lệnh tiêu tốn nhiều clock của CPU (lệnh mất nhiều thời gian thực hiện). Bạn có thể thêm số lần DIV nếu cảm thấy cần thiết. Cuối cùng, loạt lệnh POP hoàn trả lại nội dung các thanh ghi.

Bước 2. Chép tập tin SE.ASM từ thư mục \BP\SYS vào thư mục \BP\CRT. Tập tin SE.ASM chứa một số định nghĩa cho quá trình dịch các Unit của Borland (hay Turbo) Pascal.

Bước 3. Tiến hành dịch CRT.ASM bằng lệnh:

\BP\BIN\TASM CRT.ASM

Máy sẽ phát ra một vài cảnh báo về cách sử dụng tên biến. Cứ "nhắm mắt làm ngơ", chẳng có gì nghiêm trọng đâu.

Bước 4. Chạy Borland Pascal và tiến hành dịch (bấm F9) \BP\CRT\CRT.PAS để tạo ra CRT.TPU. Chép CRT.TPU vào thư mục \BP\BIN.

Bước 5. Chuyển thư mục hiện thời qua \BP\BIN

Xóa unit CRT trong TURBO.TPL bằng lệnh:

TPUMOVER TURBO.TPL -CRT

Sau đó, đưa unit CRT mới sửa vào TURBO.TPL

TPUMOVER TURBO.TPL +CRT

TURBO.TPL đã sẵn sàng. Hãy khởi động lại Borland Pascal. Hy vọng rằng mọi hàm của bạn đều được thực hiện một cách chính xác và Turbo Pascal sẽ chẳng còn phát ra những thông báo lỗi khó chịu "Run time error 200" khi bạn sử dụng unit CRT nữa.

Giải pháp cho các chương trình Pascal đã biên dịch nhưng không còn source

Chắc rằng trong số các chương trình bạn đã biên dịch bằng Borland Pascal 7, một số chương trình sử dụng unit CRT cũng sẽ gặp phải lỗi Division Error kể trên. Nếu các bạn còn giữ source, biên dịch lại chương trình với unit TURBO.TPL đã hiệu chỉnh không phải là "chuyện lớn". Thế nhưng phải làm thế nào nếu bạn không còn hay không có source các chương trình trên.

Để ý rằng do chương trình EXE đã được dịch, việc thay đổi vòng lặp DelayLoop để không ảnh hưởng tới các phần khác của chương trình là không thực hiện được. Do đó, chúng ta cần thay đổi trực tiếp đoạn mã lệnh gây lỗi.

Giải pháp tôi xin đề cử tới các bạn là thay đoạn lệnh:

XXXX:008e: mov CX,55

XXXX:0091: div CX

bằng một lệnh đơn giản: mov AX,0FFFFh vì FFFFh là giá trị tối đa mà DelayCount (biến dạng word 2 byte) có thể nhận được. Tuy nhiên, để không làm biến đổi địa chỉ các phần khác, bạn nhớ đặt thêm 1 lệnh 2 byte trước lệnh move này (do 2 lệnh nguyên thủy chiếm 5 byte bộ nhớ còn lệnh mov AX,0FFFFh chỉ chiếm 3 byte. Giải pháp này dẫn tới việc nếu bạn dùng hàm delay, máy tính sẽ chờ ít hơn (nhanh hơn) so với thời gian bạn yêu cầu, tuy nhiên trong đa số các trường hợp, điều này không dẫn tới hậu quả gì nghiêm trọng. Để tránh sự phiền hà khi tìm kiếm và thay đổi phần mã gây lỗi, tôi có kèm thêm chương trình PATCH.EXE (có thể chép tại Tòa Soạn PCW). Chương trình này sẽ tự động thực hiện việc thay đổi đoạn mã kể trên. Chỉ có đôi điều bạn cần lưu ý:

1. Chỉ dùng PATCH với các chương trình phát sinh lỗi khi chạy như kể trên.

2. Hãy lưu file nguyên thủy trước khi thực hiện thay đổi.



Borland Turbo Pascal 7 là bộ biên dịch ngôn ngữ lập trình Pascal, nhưng đã nhiều năm, hãng Borland không cập nhật. Trong bộ biên dịch Pascal này có unit CRT mà hầu hết chương trình viết trên Pascal có sử dụng. Các chương trình được biên dịch bằng Borland Turbo Pascal 7 có sử dụng unit CRT sẽ bị kết thúc với thông báo lỗi "Runtime Error 200" trên các máy PC tốc độ cao, ví dụ như máy Pentium II 350MHz bus 100MHz (chúng tôi cũng đã thử nghiệm trên máy Pentium II 350MHz - bus 66MHz thì không thấy lỗi này).

Chương trình được biên dịch bằng Borland Pascal 6 không bị lỗi này, nhưng thời gian trì hoãn trong lệnh delay bị sai hoàn toàn khi chạy trên các máy PC nhanh.

Nguyên nhân

Lỗi xảy ra khi thi hành chương trình có sự khởi tạo unit Crt, trong đó thủ tục delay cũng được khởi tạo. Việc khởi tạo thủ tục delay không chỉ được gọi trong các chương trình có sử dụng thủ tục delay, mà nó còn được gọi trong tất cả các chương trình có sử dụng unit Crt.

Khi khởi tạo thủ tục delay, chương trình sẽ đếm số lần thực hiện một vòng lặp nhỏ trong khoảng thời gian 55 mili giây (thời gian được đo bằng cách đọc bộ đếm thời gian của BIOS ở địa chỉ 40:6C, bộ đếm này nhịp 18,2 lần mỗi giây, nghĩa là mỗi nhịp chiếm 55 mili giây).

Số đếm này được chia lại cho 55 để được số lần thực hiện vòng lặp trong một mili giây. Kết quả được ghi vào trong biến kiểu word (16 bit). Kết quả sẽ được sử dụng làm cơ sở để tính thời gian trì hoãn trong lệnh delay. Khi chạy chương trình trên các máy PC tốc độ cao, số lần lặp trong 55 mili giây quá lớn (hơn 65.535) làm biến này bị tràn (overflow) và gây ra lỗi (ý nghĩa của Runtime Error 200 là lỗi chia cho Zero, nhưng thật ra đó là lỗi tràn biến).

Giải pháp

Dưới đây là vài bộ chương trình có thể tìm trên Internet để khắc phục lỗi này cho những trường hợp tiêu biểu. Cũng cần lưu ý với các bạn rằng đây là những chương trình do người dùng viết, còn Borland cũng như Intel không đưa ra giải pháp nào!

Nếu bạn chỉ có tập tin đã dịch sang file .EXE mà không có chương trình nguồn:

Có thể dùng TpPatch.zip, bộ chương trình sửa trực tiếp trên tập tin thực thi (.EXE) được biên dịch từ Borland Pascal 7, chỉ nên sử dụng cho các chương trình không cần định thời chính xác. Bạn hãy giải nén tập tin này, sau đó thực hiện theo những hướng dẫn cụ thể trong đó.

Nếu bạn sử dụng Turbo Pascal 7.01 hoặc Borland Pascal 7.01 ở chế độ real mode :Trong trường hợp này, bạn có thể tải xuống ba tập tin sau và tùy chọn theo ý bạn.

T7TplFix.zip. Bộ chương trình nâng cấp cho tập tin thư viện Run Time Library (TURBO.TPL), bạn chỉ cần giải nén, chạy chương trình và nó sẽ tự động thực hiện công việc trên.

NewDelay.pas. Là một tập tin dạng Unit thay thế lệnh delay. Nếu dùng Unit này, phải khai báo nó trước Crt và tất cả các Unit khác có sử dụng Unit Crt trong chương trình nguồn.

Rdelay.zip. Giống như NewDelay, nó thay thế lệnh Delay.

Bộ chương trình bppatch. Đây là giải pháp theo chúng tôi thấy là tốt nhất khi tiến hành các thử nghiệm. Giải pháp này bao gồm chương trình nguồn Crt.asm và Crt.pas (mà chúng ta có thể học được nhiều điều nếu so sánh Crt.asm và Crt.pas với chương trình gốc của Borland) cùng các thư viện Turbo.tpl, Tpp.tpl, Tpw.tpl. Hoặc là bạn dịch lại toàn bộ thư viện RTL, hoặc đơn giản là bạn chỉ cần chép các file .TPL vào thư mục BIN và dịch lại các chương trình mà bạn đã viết.

Vài bộ chương trình khác như bp7patch, tp7p5fix, tpbug.

Sau khi tìm hiểu các giải pháp trên, nhiều người trong chúng ta có thể thấy rằng một giải pháp đầy đủ về nhiều mặt (nhất là vấn đề bản quyền) vẫn chưa có, trừ khi Borland (đã đổi tên thành Inprise) phát hành phiên bản mới.

No comments:

Post a Comment