INT 21h AH=40h writes CX bytes from DS:DX at the file pointer and moves the pointer past them. Writing past the end makes the file longer; writing in the middle overwrites what was there. AX returns the number of bytes written.
Check AX, not only CF. When the disk fills up DOS writes what fits and returns CF = 0 with a smaller AX, so a program that tests only CF loses data quietly. CF = 1 with 05h means the handle was opened read-only, 06h that the handle is not open.
Standard handles
| BX | Goes to | Notes |
|---|---|---|
| 1 | standard output | the screen, or a file after PROG > OUT.TXT |
| 2 | standard error | the screen; COMMAND.COM cannot redirect it |
| 3 | AUX | the first serial port |
| 4 | PRN | the first printer port |
Writing to handle 1 is the general way to print. Unlike INT 21h AH=09h it takes a length instead of looking for a terminator, so the text may contain "$", and it can be up to 64 KB long:
MOV AH, 40h ; DOS: write to a handleMOV BX, 1 ; 1 = standard outputMOV CX, MSG1L ; a length: 40h does not look for "$"MOV DX, OFFSET MSG1INT 21hMOV AH, 40hMOV BX, 2 ; 2 = standard errorMOV CX, MSG2LMOV DX, OFFSET MSG2INT 21hMSG1 DB "a $ sign is just a byte here", 13, 10MSG1L EQU $ - MSG1MSG2 DB "and this went to handle 2", 13, 10MSG2L EQU $ - MSG2
Writing 0 bytes
CX = 0 writes nothing and sets the length of the file to the position of the file pointer. With the pointer inside the file the rest is cut off; with the pointer moved past the end by INT 21h AH=42h, the file grows to that length (DOS gives it the clusters but does not clear them, so the new bytes are whatever was on the disk). It is the only way DOS offers to shorten a file without recreating it.
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 ; DOS: write 12 bytesMOV CX, 12MOV DX, OFFSET HELLOINT 21hCALL CHECKMOV AX, 4200h ; file pointer back to offset 5XOR CX, CXMOV DX, 5INT 21hCALL CHECKMOV AH, 40h ; write 0 bytes: the file ends here nowXOR CX, CXINT 21hCALL CHECKMOV AX, 4202h ; to the end: DX:AX = the lengthXOR CX, CXXOR DX, DXINT 21hCALL CHECKMOV SI, AX ; SI = the lengthMOV AH, 3Eh ; close, then open again to read itINT 21hMOV AX, 3D00h ; DOS: open TEST.TXT for readingMOV DX, OFFSET FNAMEINT 21hCALL CHECKMOV BX, AX ; BX = the new handleMOV AH, 3Fh ; read what is leftMOV CX, 12MOV DX, OFFSET BUFINT 21hCALL CHECKMOV DI, AX ; DI = bytes readMOV AH, 3Eh ; close the handleINT 21hMOV AH, 41h ; delete TEST.TXTMOV DX, OFFSET FNAMEINT 21hMOV 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, DIMOV DX, OFFSET BUFCALL OUT1MOV DX, OFFSET CRLFMOV AH, 09h ; DOS: print the string at DS:DXINT 21hFNAME DB "TEST.TXT", 0HELLO DB "Hello, world"M1 DB "size after the 0-byte write: $"M2 DB ", contents: $"CRLF 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
The example closes the file and opens it again before reading it back. In DOSBox-X, on a drive that is a folder of the host (as C: is here), the new length shows at once, but a read through the same handle still returns the bytes that were cut off until the file is opened again; on real DOS the read stops at the new end either way.
On a device a write of 0 bytes does nothing. Handles 1 and 2 both lead to the CON device, which in the workbench feeds the DOS screen and the terminal alike.