12

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 [email protected] 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 FILE_ALIGNMENT_SHIFT equ 1 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. ..@code: %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 %else .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. %endif .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. %endif ; 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-$$ .end: PFLAG: .INST equ 2 AFLAG: .WINCOMPAT equ 2 OSFLAG: .WINDOWS equ 2 ; Win16. ne_header: .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 .SegAlignShift dw FILE_ALIGNMENT_SHIFT .ResourceSegCnt dw 0 .OsFlags db OSFLAG.WINDOWS .ExeFlags db 0 .FastLoadOfs dw 0 .FastLoadSize dw 0 .Reserved dw 0 .ExpectedWinVer dw 0x300 $SEG: .DATA equ 0x0001 .MOVABLE equ 0x0010 .SHARABLE equ 0x0020 .PRELOAD equ 0x0040 .REL equ 0x0100 .DPL3 equ 0x0c00 .DISCARDABLE equ 0x1000 SegTab: .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 .end: ResourceTab: .end: ResNameTab: .entry0 db .entry0.end-.entry0-3, 'hellnw16', 0, 0 ; Module name. .entry0.end: db 0 ; End of table. ModRefTab: .user: dw ImpNameTab.mod1-ImpNameTab .kernel: dw ImpNameTab.mod2-ImpNameTab .end: ImpNameTab: db 0 .mod1 db .mod1.end-$-1, 'USER' .mod1.end: .mod2 db .mod2.end-$-1, 'KERNEL' .mod2.end: EntryTab: db 0 ; Why? db 0 .end: NonResNameTab: .entry0: db .entry0.end-.entry0-3, 'hellnw16.exe', 0, 0 ; Module description. .entry0.end: db 0 ; End of table. .end: before_segment_code times ($$-$)&((1<<FILE_ALIGNMENT_SHIFT)-1) db 0 segment_code: segment_data: instancedata: 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 segment_code_end: $AT: ; .AddrType. .OFFSET equ 1 .SEGMENT equ 2 .FARPTR equ 3 .OFFSET16 equ 5 REL: ; .RelType. .IMPORDINAL equ 1 .IMPNAME equ 2 segment_code_relocs: .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: ; __END__ 
5
  • 2
    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?CommentedDec 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
    CommentedDec 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.CommentedDec 21, 2022 at 2:32
  • 3
    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.CommentedDec 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".CommentedDec 21, 2022 at 13:53

1 Answer 1

13

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.

8
  • 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
    CommentedDec 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’.CommentedDec 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
    CommentedDec 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
    CommentedDec 21, 2022 at 18:01
  • 3
    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.CommentedDec 21, 2022 at 20:21

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.