INT 21h AH=3Dh finds an existing file and opens it. Nothing about the file changes: the contents and the length stay, and the file pointer starts at offset 0, so the first read gets the first byte and the first write overwrites it. To append, open and then move the pointer to the end with INT 21h AH=42h. If the file is not there the call fails; making files is the job of INT 21h AH=3Ch.

ALMeaning
00hread only: a write through this handle fails with 05h
01hwrite only: a read fails with 05h
02hread and write
bits 4-6sharing (DOS 3+): 00h compatibility, 10h deny others all access, 20h deny write, 30h deny read, 40h deny nothing
bit 7 (80h)private: a program started with EXEC does not inherit the handle

The sharing bits matter only with SHARE.EXE loaded or on a network, where they decide whether a second open of the same file succeeds; ordinary programs use 00h, 01h or 02h. A value of 3 or more in bits 0-2 is error 0Ch, invalid access code.

The name can also be a device: opening CON, NUL or PRN gives a handle to the device, and reads and writes go to it. INT 21h AH=44h tells a device from a file.

Errors

AXMeaning
02hfile not found: the directory is there, the file is not
03hpath not found: a directory in the name does not exist
04htoo many open files: all 20 handles of the program are in use, or the system table (FILES=) is full
05haccess denied: AL asks to write a read-only file, or the name is a directory
0Chinvalid access code in AL
prints two lines: "NO_SUCH.TXT: error 02" and "NO_SUCH\TEST.TXT: error 03"
MOV DX, OFFSET M1
MOV AH, 09h ; DOS: print the string at DS:DX
INT 21h
MOV AX, 3D00h ; DOS: open, AL = 0: for reading
MOV DX, OFFSET NAME1 ; a file that is not there
INT 21h
CALL REPORT
MOV DX, OFFSET M2
MOV AH, 09h ; DOS: print the string at DS:DX
INT 21h
MOV AX, 3D00h
MOV DX, OFFSET NAME2 ; in a directory that is not there
INT 21h
CALL REPORT
NAME1 DB "NO_SUCH.TXT", 0
NAME2 DB "NO_SUCH\TEST.TXT", 0
M1 DB "NO_SUCH.TXT: $"
M2 DB "NO_SUCH\TEST.TXT: $"
CRLF DB 13, 10, "$"
REPORT PROC NEAR ; CF = 0: "ok"; CF = 1: "error" and AL
JC REPORT1
MOV DX, OFFSET OKMSG
MOV AH, 09h
INT 21h
RET
REPORT1:
PUSH AX
MOV DX, OFFSET ERRMSG
MOV AH, 09h
INT 21h
POP AX
CALL PHEXB ; the error code
MOV DX, OFFSET CRLF
MOV AH, 09h
INT 21h
RET
REPORT ENDP
OKMSG DB "ok", 13, 10, "$"
ERRMSG DB "error $"
PHEXB PROC NEAR ; print AL as two hex digits
PUSH CX
PUSH DX
MOV DH, AL ; keep the byte
MOV CL, 4
SHR AL, CL ; the high digit first
MOV CX, 2
PHEXB1:
AND AL, 0Fh
ADD AL, "0"
CMP AL, "9"
JBE PHEXB2
ADD AL, 7 ; 10..15 -> "A".."F"
PHEXB2:
MOV DL, AL
MOV AH, 02h ; DOS: print DL
INT 21h
MOV AL, DH ; then the low digit
LOOP PHEXB1
POP DX
POP CX
RET
PHEXB ENDP

The access mode is checked on every read and write, not only at the open. Here a file opened with AL = 0 refuses a write and allows a read:

prints two lines: "write through a read-only handle: error 05" and "read back: DOS"
MOV AH, 3Ch ; DOS: create TEST.TXT
XOR CX, CX ; CX = 0: normal attributes
MOV DX, OFFSET FNAME
INT 21h
CALL CHECK ; stop if CF = 1
MOV BX, AX ; BX = the handle
MOV AH, 40h ; put "DOS" in it
MOV CX, 3
MOV DX, OFFSET TXT1
INT 21h
CALL CHECK
MOV AH, 3Eh ; close
INT 21h
MOV AX, 3D00h ; DOS: open it again, read only
MOV DX, OFFSET FNAME
INT 21h
CALL CHECK
MOV BX, AX
MOV DX, OFFSET M1
MOV AH, 09h ; DOS: print the string at DS:DX
INT 21h
MOV AH, 40h ; try to write 3 bytes
MOV CX, 3
MOV DX, OFFSET TXT1
INT 21h
CALL REPORT ; CF = 1, AX = 05h
MOV AH, 3Fh ; reading is allowed
MOV CX, 3
MOV DX, OFFSET BUF
INT 21h
CALL CHECK
MOV SI, AX ; SI = bytes read
MOV DX, OFFSET M2
MOV AH, 09h ; DOS: print the string at DS:DX
INT 21h
MOV CX, SI
MOV DX, OFFSET BUF
CALL OUT1 ; print them
MOV DX, OFFSET CRLF
MOV AH, 09h ; DOS: print the string at DS:DX
INT 21h
MOV AH, 3Eh ; close the handle
INT 21h
MOV AH, 41h ; delete TEST.TXT
MOV DX, OFFSET FNAME
INT 21h
FNAME DB "TEST.TXT", 0
TXT1 DB "DOS"
M1 DB "write through a read-only handle: $"
M2 DB "read back: $"
CRLF DB 13, 10, "$"
BUF DB 64 DUP (?)
CHECK PROC NEAR ; CF = 1: print the error code in AL and exit
JNC CHECK1
PUSH AX
MOV DX, OFFSET FAILMSG
MOV AH, 09h
INT 21h
POP AX
CALL PHEXB
MOV AX, 4C01h ; exit with code 1
INT 21h
CHECK1:
RET
CHECK ENDP
FAILMSG DB "failed: error $"
REPORT PROC NEAR ; CF = 0: "ok"; CF = 1: "error" and AL
JC REPORT1
MOV DX, OFFSET OKMSG
MOV AH, 09h
INT 21h
RET
REPORT1:
PUSH AX
MOV DX, OFFSET ERRMSG
MOV AH, 09h
INT 21h
POP AX
CALL PHEXB ; the error code
MOV DX, OFFSET CRLF
MOV AH, 09h
INT 21h
RET
REPORT ENDP
OKMSG DB "ok", 13, 10, "$"
ERRMSG DB "error $"
PHEXB PROC NEAR ; print AL as two hex digits
PUSH CX
PUSH DX
MOV DH, AL ; keep the byte
MOV CL, 4
SHR AL, CL ; the high digit first
MOV CX, 2
PHEXB1:
AND AL, 0Fh
ADD AL, "0"
CMP AL, "9"
JBE PHEXB2
ADD AL, 7 ; 10..15 -> "A".."F"
PHEXB2:
MOV DL, AL
MOV AH, 02h ; DOS: print DL
INT 21h
MOV AL, DH ; then the low digit
LOOP PHEXB1
POP DX
POP CX
RET
PHEXB ENDP
OUT1 PROC NEAR ; write CX bytes at DS:DX to handle 1
PUSH BX
MOV BX, 1 ; standard output
MOV AH, 40h
INT 21h
POP BX
RET
OUT1 ENDP

Pitfalls

Test CF before using AX. On failure AX holds the error code, a small number that looks like a perfectly good handle (2 is standard error), and writing to it puts your data on the screen. Handles are also a limited resource: a program that opens files in a loop and forgets to close them gets 04h after about fifteen.

DOSBox-X accepts the sharing bits but passes them on to the host only on Windows. On a host folder, whether a read-only file can really be written also depends on the host's file permissions.