OFFSET label is the label's address within its segment, as a number. SEG label is the segment the label lives in. Both are computed when the program is assembled and linked, not when it runs, so MOV DX, OFFSET MSG1 is a move of an immediate constant. With .MODEL, MASM measures offsets from the start of the group (DGROUP), which is what DS points at.
MOV DX, OFFSET MSG1 ; DX = the address of MSG1MOV AH, 09h ; DOS: print the string at DS:DXINT 21hMOV AX, OFFSET MSG2 - OFFSET MSG1 ; distance between two labelsADD AL, '0' ; digit -> characterMOV DL, ALMOV AH, 02h ; DOS: print DLINT 21hMSG1 DB 'Hi!', 13, 10, '$'MSG2 DB 0
The classic mistake
Without OFFSET, MASM treats a data label as the memory it names. MOV DX, MSG1 loads the first two bytes of the string ("Hi") into DX, not its address, and the following print shows garbage. Whenever you want a pointer, write OFFSET (or use LEA).
OFFSET or LEA
MOV reg, OFFSET label is one byte shorter than LEA reg, label and gives the same value. LEA is needed when registers take part in the address, which OFFSET cannot express since it is a constant.
SEG
A segment value is not known until DOS loads the program, so SEG leaves a relocation that the loader patches in a .EXE. A .COM has no relocation table: there SEG cannot be used, and you take the segment from CS at run time instead. In a .EXE the usual start looks like this:
MOV AX, SEG MSG1 ; .EXE only: patched by the loaderMOV DS, AX ; (the same as MOV AX, @data with .MODEL)MOV DX, OFFSET MSG1
Pitfalls
OFFSET gives an offset, not a full address: it means something only together with the right segment register. DW label stores the offset like DW OFFSET label; DD label stores offset and segment and so needs a relocation.