I wrote a simple Win16 program in NASM assembly. It works on Windows 3.11. (Source code: https://github.com/pts/mininasm/blob/master/demo/hello/helljw16.nasm .)

When I tried to combine the code and data segments to a single segment, it stopped working. I don't get any error message from Windows, but the messagebox doesn't appear. Is it possible to have a combined segment? If so, what's wrong with my code?

Update: Please note that my code has very few read-write variables (currently none, except in registers and on the stack), so it would be OK to keep the read-only variables in the code segment, and use a few bytes of stack (populated manually) for the read-write variables. Thus the data segment entry in SegTab is not strictly necessary. But the program doesn't work without it, and I'm interested if there is a fix.

The relevant part below is after the SegTab label: it contains a definition for a single segment only.

; hellhw16.nasm: minimalistic hello-world Win16 Windows 3.0 8086 .exe
; by pts@fazekas.hu at Tue Dec 20 22:40:03 CET 2022
; nasm -O0 -f bin -o helljw16.exe helljw16.nasm
; See also: https://github.com/alexfru/Win16asm/blob/master/ne.inc
; See also: https://board.flatassembler.net/topic.php?t=21339
; See also: https://devblogs.microsoft.com/oldnewthing/20071203-00/?p=24323

        bits 16
        cpu 8086
        org 0


mz_header:  ; DOS .exe header: http://justsolve.archiveteam.org/wiki/MS-DOS_EXE
.signature  db 'MZ'
.lastsize:      dw (.end - .signature) & 0x1ff  ; The value 0 and 0x200 are equivalent here. Microsoft Linker 3.05 generates 0, so do we. Number of bytes in the last 0x200-byte block in the .exe file.
.nblocks:       dw (.end - .signature  + 0x1ff) >> 9  ; Number of 0x200-byte blocks in .exe file (rounded up).
.nreloc     dw 0  ; No relocations.
.hdrsize    dw 0  ; Load .exe file to memory from the beginning.
%if 1  ; Produces identical .exe output even if we change it to 0.
.minalloc:  mov ax, 0x903  ; AH := 9, AL := junk. The number 3 matters for minalloc, see below.
.ss_minus_1:    mov dx, .message+(0x100-$$)  ; (0x100-$$) to make it work with any `org'.
.sp:        int 0x21  ; Print the message to stdout. https://stanislavs.org/helppc/int_21-9.html
.checksum:  jmp strict short .do_exit
.minalloc   dw 0x03b8  ; To have enough room for the stack, we need minalloc >= ss+((sp+0xf)>>4)-0x20 == 0x315. kvikdos verifies it.
.maxalloc   dw 0xba09  ; Actual value doesn't matter.
.ss     dw 0x0118  ; Actual value doesn't matter as long as it matches minalloc. dw message
.sp     dw 0x21cd  ; Actual value doesn't matter. int 0x21
.checksum   dw 0x21eb  ; Actual value doesn't matter.
.ip     dw ..@code+(0x100-$$)  ; Entry point offset. (0x100-$$) to make it work with any `org'.
.cs     dw 0xfff0  ; CS := PSP upon entry.
%if 0
.relocpos   dw ?  ; Doesn't matter, overlaps with 2 bytes of message: 'He'.
.noverlay   dw ?  ; Doesn't matter. overlaps with 2 bytes of message: 'll'.
; End of 0x1c-byte .exe header.

; OpenWatcom stub message.
.message    db 'This is a Win16 executable.', 13, 10, '$'

.do_exit:   mov ax, 0x4c01  ; Same exit code as in OpenWatcom stub.
        int 0x21

        times 0x3c-($-$$) db 0  ; Pad to 60 bytes.
        dd ne_header-$$

.INST       equ 2


.WINDOWS    equ 2  ; Win16.

.Signature  db 'NE'
.LinkerVer  db 5
.LinkerRev  db 1
.EntryTabOfs    dw EntryTab-ne_header
.EntryTabSize   dw EntryTab.end-EntryTab
.ChkSum     dd 0  ; Always 0.
.ProgramFlags   db PFLAG.INST
.ApplicationFlags db AFLAG.WINCOMPAT
.AutoDataSegNo  dw 1
.HeapInitSize   dw 0
.StackSize  dw 0x100
.InitIp     dw _start-segment_code
.InitCsSegNo    dw 1
.InitSp     dw 0
.InitSsSegNo    dw 1
.SegCnt     dw (SegTab.end-SegTab)>>3
.ModCnt     dw (ModRefTab.end-ModRefTab)>>1
.NonResNameTabSize dw NonResNameTab.end-NonResNameTab
.SegTabOfs  dw SegTab-ne_header
.ResourceTabOfs dw ResourceTab-ne_header
.ResNameTabOfs  dw ResNameTab-ne_header
.ModRefTabOfs   dw ModRefTab-ne_header
.ImpNameTabOfs  dw ImpNameTab-ne_header
.NonResNameTabOfs dd NonResNameTab-$$
.MovableEntryCnt dw 0
.ResourceSegCnt dw 0
.ExeFlags   db 0
.FastLoadOfs    dw 0
.FastLoadSize   dw 0
.Reserved   dw 0
.ExpectedWinVer dw 0x300

.DATA       equ 0x0001
.MOVABLE    equ 0x0010
.SHARABLE   equ 0x0020
.PRELOAD    equ 0x0040
.REL        equ 0x0100
.DPL3       equ 0x0c00
.DISCARDABLE    equ 0x1000

.seg1.fofs  dw (segment_code-$$)>>FILE_ALIGNMENT_SHIFT
.seg1.size  dw segment_code_end-segment_code
.seg1.flags dw $SEG.REL | $SEG.PRELOAD | $SEG.REL | $SEG.DPL3 | $SEG.DATA
.seg1.minalloc  dw segment_code_end-segment_code


.entry0     db .entry0.end-.entry0-3, 'hellnw16', 0, 0  ; Module name.
        db 0  ; End of table.
.user:      dw ImpNameTab.mod1-ImpNameTab
.kernel:    dw ImpNameTab.mod2-ImpNameTab

        db 0
.mod1       db .mod1.end-$-1, 'USER'
.mod2       db .mod2.end-$-1, 'KERNEL'

        db 0  ; Why?
        db 0

.entry0:    db .entry0.end-.entry0-3, 'hellnw16.exe', 0, 0  ; Module description.
        db 0  ; End of table.

before_segment_code times ($$-$)&((1<<FILE_ALIGNMENT_SHIFT)-1) db 0
msg_hi      db 'Hi!', 0
        times (instancedata+0xa)-$ db 0
.id0xa      dw 0  ; InitTask overwrites it to stack bottom.
.id0xc      dw 0  ; InitTask overwrites it to stack bottom.
.id0xe      dw 0  ; InitTask overwrites it to stack top.

msg_hello   db 'Hello, World!', 0

_start:     push cs
        pop ds
        xor bp, bp  ; !! Do we need this? Maybe on Windows 3.0? Microsoft libc has it.
        push bp  ; Do we need this? Not on Windows 3.11.
..@reloc1   equ $+1
        call 0:0xffff  ; InitTask. 'INITTASK'.'KERNEL' @91 == @0x5b.
        test ax, ax
        jz strict short .fail
        push di
        db 0x33, 0xc0  ; xor ax, ax
        push ax
..@reloc2   equ $+1
        call 0:0xffff  ; WaitEvent. 'WAITEVENT'.'KERNEL' @30 == @0x1e.
..@reloc3   equ $+1
        call 0:0xffff  ; InitApp. 'INITAPP'.'USER' @5.
        test ax, ax
        jz strict short .fail
        xor ax, ax
        push ax
        push ds
        mov bx, msg_hello-segment_data
        push bx
        push ds
        mov bx, msg_hi-segment_data
        push bx
        push ax
..@reloc0   equ $+1
        call 0x0:0xffff  ; MessageBox. 'MESSAGEBOX'.'USER' @1. !! Why is segment -0?
        mov ax, 0x4c00  ; EXIT_SUCCESS == 0.
        int 0x21
.fail:      mov ax,0x4c01  ; EXIT_FAILURE == 1.
        int 0x21


$AT:  ; .AddrType.
.OFFSET     equ 1
.SEGMENT    equ 2
.FARPTR     equ 3
.OFFSET16   equ 5

REL:  ; .RelType.
.IMPNAME    equ 2

.count:     dw (.end-.count-2)>>3  ; Each relocation is 8 bytes, 1 line each below.
        dw $AT.FARPTR | REL.IMPORDINAL<<8, ..@reloc0  -segment_code, (ModRefTab.user  -ModRefTab+2)>>1, 1     ; 'MESSAGEBOX'.'USER' @1.
        dw $AT.FARPTR | REL.IMPORDINAL<<8, ..@reloc1  -segment_code, (ModRefTab.kernel-ModRefTab+2)>>1, 0x5b  ; 'INITTASK'.'KERNEL' @91 == @0x5b.
        dw $AT.FARPTR | REL.IMPORDINAL<<8, ..@reloc2  -segment_code, (ModRefTab.kernel-ModRefTab+2)>>1, 0x1e  ; 'WAITEVENT'.'KERNEL' @30 == @0x1e.
        dw $AT.FARPTR | REL.IMPORDINAL<<8, ..@reloc3  -segment_code, (ModRefTab.user  -ModRefTab+2)>>1, 5     ; 'INITAPP'.'USER' @5.

; __END__
  • 1
    x86 protected-mode selectors can be either writable or executable, never both. (Though nothing stops you from creating multiple selectors to the same memory.) I suspect the problem here ultimately boils down to that, as a single NE segment corresponds to a single selector. Have you tried running the program in real-mode Windows? Dec 21, 2022 at 2:09
  • 1
    @user3840170: This is a Win16 application, I'm using only the 8086 instruction set. I'm trying it in Windows 3.11. Windows 3.11 probably runs in protected mode, because the 386 Enhanced section in the Control Panel works. I think it's very unlikely that Windows 3.11 is running my program in protected mode, because I didn't ask for it anywhere. (It may run the program in Virtual 8086 mode, but selectors don't matter then.)
    – pts
    Dec 21, 2022 at 2:14
  • 2
    Windows 3 only ever runs DOS apps in virtual 8086 mode. If the system is running in protected mode, all Windows apps are as well. Dec 21, 2022 at 2:32
  • 2
    Windows 3.0 supported real mode, standard mode (ie. 286 protected mode) and 386 enhanced mode. Windows 3.1 supported standard mode and 386 enhanced mode. Windows for Workgroups 3.11 only supported 386 enhanced mode.
    – idspispopd
    Dec 21, 2022 at 8:37
  • For reference, setting CS=DS is the "tiny" memory model, and it's for CP/M backwards compatibility. See devblogs.microsoft.com/oldnewthing/20200728-00/?p=104012 and gist.github.com/berk76/02fa06d4628b3d1493fee41c80dd26ad. Other memory models were "small", "medium", "compact", "large" and "huge". Dec 21, 2022 at 13:53

1 Answer 1


I don’t think this is supposed to be possible at all.

One reason why is that Windows may decide to run your program in protected mode. Your program obviously needs to have one executable segment to keep your code, and one writeable segment for the stack, which must be referred to in the field named .InitSsSegNo in the question (zero is only allowed for DLLs). And in protected mode, a segment selector can be executable or writeable, but not both. You simply cannot have CS=DS=SS in protected mode.

Okay, fine, in principle you can have more than one selector pointing to the same memory, with different access rights; the CPU alone will not stop you. But the Win16 programming model and the New Executable format do not provide for such a situation. Each segment must be declared as either code or data, and code segments may be entirely evicted from memory, to be later reloaded from the original executable file; there were no swap files to keep any modifications.0 For this to work well, the executable must contain fresh data, which is to say, the code segment cannot have been written into. And this is just as true in real-mode Windows as it is in protected mode.

That’s not to say you can’t save a couple of bytes in your tiny executable as it is. You can move the string constants into the code segment, then declare the data segment as not stored within the executable (essentially, make it a BSS segment) by setting the fofs and size fields of the segment table entry to zero. This saves 13 bytes. Other than that, though, I’m not seeing much way to save space without cutting too many corners.

0 See The Old New Thing posts: on GlobalLock and on segment tuning. I must admit I have a hard time finding a resource more authoritative and in-depth, so perhaps I am missing some obscure detail here that could make it work. Still, that was the general case at least.

  • If I set the size field to 0, how does Windows know how many bytes to allocate? Will it just use 0, and then put the heap (of the specified size) there?
    – pts
    Dec 21, 2022 at 12:55
  • 1
    From the field you named .minalloc. See <bytepointer.com/resources/win16_ne_exe_format_win3.0.htm>, section ‘Segment table’. Dec 21, 2022 at 13:26
  • Thank you for the tips, I used them to make the program file shorter (github.com/pts/mininasm/blob/master/demo/hello/helljw16.nasm) without merging the segments.
    – pts
    Dec 21, 2022 at 17:28
  • Your answer contains a convincing argument why merging a read-execute code segment and a read-write data segment to a read-write-execute segment (even if it's done with two selectors) won't work on Windows 3.x. However, in theory it's possible to solve my problem by removing the data segment (from the SegTab), because I don't have any read-write variables there. See also the Update in the question. Do you have any insights how that could work?
    – pts
    Dec 21, 2022 at 18:01
  • 2
    The field you named InitSsSegNo in the NE header is an index of an entry in the segment table. It must point to a data segment (0 is only allowed for DLLs), and it cannot be the same as the code segment. So there have to be two segments at least. Dec 21, 2022 at 20:21

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .