INT 21h AH=3Fh copies bytes from the file pointer's position into the buffer at DS:DX and moves the pointer past them. CX can be anything from 0 to FFFFh, and the buffer must have room for that many bytes. AX returns how many were really read.
The end of the file is not an error. A read that runs into it returns what was left, so AX is less than CX, and every read after that returns AX = 0 with CF = 0. A loop over a file therefore stops when AX is 0 (or smaller than CX), not when CF is set. CF = 1 means something went wrong: 05h if the handle was opened write-only, 06h if it is not open at all.
MOV AH, 3Ch ; DOS: create TEST.TXTXOR CX, CX ; CX = 0: normal attributesMOV DX, OFFSET FNAMEINT 21hCALL CHECK ; stop if CF = 1MOV BX, AX ; BX = the handleMOV AH, 40h ; put ten letters in itMOV CX, 10MOV DX, OFFSET LETTERSINT 21hCALL CHECKMOV AH, 3EhINT 21hMOV AX, 3D00h ; open it for readingMOV DX, OFFSET FNAMEINT 21hCALL CHECKMOV BX, AXAGAIN:MOV AH, 3Fh ; DOS: read up to 4 bytesMOV CX, 4MOV DX, OFFSET BUFINT 21hCALL CHECKMOV SI, AX ; SI = how many cameCALL PDEC ; print that numberMOV DX, OFFSET LBRMOV AH, 09h ; DOS: print the string at DS:DXINT 21hMOV CX, SIMOV DX, OFFSET BUFCALL OUT1 ; and the bytes themselvesMOV DX, OFFSET RBRMOV AH, 09h ; DOS: print the string at DS:DXINT 21hOR SI, SI ; 0 bytes: the end of the fileJNZ AGAINMOV AH, 3Eh ; close the handleINT 21hMOV AH, 41h ; delete TEST.TXTMOV DX, OFFSET FNAMEINT 21hFNAME DB "TEST.TXT", 0LETTERS DB "ABCDEFGHIJ"LBR DB " [$"RBR DB "]", 13, 10, "$"BUF DB 64 DUP (?)CHECK PROC NEAR ; CF = 1: print the error code in AL and exitJNC CHECK1PUSH AXMOV DX, OFFSET FAILMSGMOV AH, 09hINT 21hPOP AXCALL PHEXBMOV AX, 4C01h ; exit with code 1INT 21hCHECK1:RETCHECK ENDPFAILMSG DB "failed: error $"PHEXB PROC NEAR ; print AL as two hex digitsPUSH CXPUSH DXMOV DH, AL ; keep the byteMOV CL, 4SHR AL, CL ; the high digit firstMOV CX, 2PHEXB1:AND AL, 0FhADD AL, "0"CMP AL, "9"JBE PHEXB2ADD AL, 7 ; 10..15 -> "A".."F"PHEXB2:MOV DL, ALMOV AH, 02h ; DOS: print DLINT 21hMOV AL, DH ; then the low digitLOOP PHEXB1POP DXPOP CXRETPHEXB ENDPPDEC PROC NEAR ; print AX in decimalPUSH BXPUSH CXPUSH DXMOV BX, 10XOR CX, CX ; CX = digits pushedPDEC1:XOR DX, DXDIV BX ; AX = AX / 10, DX = the last digitPUSH DXINC CXOR AX, AXJNZ PDEC1PDEC2:POP DX ; the digits come back first to lastADD DL, "0"MOV AH, 02hINT 21hLOOP PDEC2POP DXPOP CXPOP BXRETPDEC ENDPOUT1 PROC NEAR ; write CX bytes at DS:DX to handle 1PUSH BXMOV BX, 1 ; standard outputMOV AH, 40hINT 21hPOP BXRETOUT1 ENDP
Files are read as raw bytes: CR, LF and Ctrl+Z (1Ah) come through like any other byte. Text conventions are the program's business.
Reading handle 0
Handle 0 is standard input. When it is the console, 3Fh reads a line from the keyboard with the editing keys of INT 21h AH=0Ah: it echoes what is typed, waits for Enter, and returns the line with CR and LF on the end, at most CX bytes of it. If the line is longer than CX, the rest stays in DOS's buffer and the next 3Fh gets it without waiting. Ctrl+C on the line ends the program, as with the other console services.
Run with its input redirected (PROG < IN.TXT), the same program finds a file behind handle 0, and 3Fh reads it like any file: no echo, no waiting, AX = 0 at the end. Reading input through handle 0 therefore works both ways without the program knowing which; INT 21h AH=44h tells it, if it needs to know. The example puts "Hi" and Enter into the keyboard buffer first, so it does not wait:
MOV AH, 05h ; BIOS: put a key into the keyboard bufferMOV CX, 2348h ; CH = scan code, CL = "H"INT 16hMOV AH, 05hMOV CX, 1769h ; "i"INT 16hMOV AH, 05hMOV CX, 1C0Dh ; EnterINT 16hMOV AH, 3Fh ; DOS: read from handle 0XOR BX, BX ; BX = 0: standard inputMOV CX, 20 ; room for 20 bytesMOV DX, OFFSET LINE1INT 21hCALL CHECKMOV SI, AX ; SI = bytes read, CR and LF includedMOV DX, OFFSET M1MOV AH, 09h ; DOS: print the string at DS:DXINT 21hMOV AX, SICALL PDECMOV DX, OFFSET M2MOV AH, 09h ; DOS: print the string at DS:DXINT 21hMOV CX, SIMOV DX, OFFSET LINE1CALL OUT1 ; the line, with its CR LFLINE1 DB 20 DUP (?)M1 DB "got $"M2 DB " bytes: $"CHECK PROC NEAR ; CF = 1: print the error code in AL and exitJNC CHECK1PUSH AXMOV DX, OFFSET FAILMSGMOV AH, 09hINT 21hPOP AXCALL PHEXBMOV AX, 4C01h ; exit with code 1INT 21hCHECK1:RETCHECK ENDPFAILMSG DB "failed: error $"PHEXB PROC NEAR ; print AL as two hex digitsPUSH CXPUSH DXMOV DH, AL ; keep the byteMOV CL, 4SHR AL, CL ; the high digit firstMOV CX, 2PHEXB1:AND AL, 0FhADD AL, "0"CMP AL, "9"JBE PHEXB2ADD AL, 7 ; 10..15 -> "A".."F"PHEXB2:MOV DL, ALMOV AH, 02h ; DOS: print DLINT 21hMOV AL, DH ; then the low digitLOOP PHEXB1POP DXPOP CXRETPHEXB ENDPPDEC PROC NEAR ; print AX in decimalPUSH BXPUSH CXPUSH DXMOV BX, 10XOR CX, CX ; CX = digits pushedPDEC1:XOR DX, DXDIV BX ; AX = AX / 10, DX = the last digitPUSH DXINC CXOR AX, AXJNZ PDEC1PDEC2:POP DX ; the digits come back first to lastADD DL, "0"MOV AH, 02hINT 21hLOOP PDEC2POP DXPOP CXPOP BXRETPDEC ENDPOUT1 PROC NEAR ; write CX bytes at DS:DX to handle 1PUSH BXMOV BX, 1 ; standard outputMOV AH, 40hINT 21hPOP BXRETOUT1 ENDP
DOSBox-X reads the console more simply than MS-DOS: it returns as soon as CX characters have been typed, even before Enter, and draws its echo through the BIOS. The echo is therefore on the DOS screen but not in the workbench terminal, which shows what programs write to CON.