User Tools

Site Tools


blog:pushbx:2022:0826_dual_code_segments_mechanisms

Dual code segments mechanisms

The macros for dual code segment support contain two different macros for differently handling inter-segment calls.

The easier to use is the nearcall macro. If calling within the same segment it simply expands to a near immediate call (opcode 0E8h). Otherwise it calls a helper function, with a target offset to call stored in the code behind the helper function call opcode. This helper thunk will rearrange the stack so that it branches far to the desired target, with two return addresses on the stack. First is a near return address. This will return to a small helper in the same code segment as the target. This helper will use the second return address, which is far, to return to the original caller in the other segment. (The second return address is actually "far-like". Details to follow in the lDebugX dual call description.)

The nearcall mechanism only works if the called target function does not access parameters on its stack. If such access is desired a dualcall must be used instead. The following first describes 86-Mode-only dual calls, which cannot be used by DPMI-capable (lDebugX) builds.

The dualcall macro will expand to a push cs then call near rel16 sequence if calling into the same code segment. This sequence constructs a stack frame matching that of a far call. When calling into the other code segment, the macro expands into a call far imm16:imm16 instruction, with zero used as the segment. The segment word of this instruction is entered into a relocation patch table, which is used by init to fill in the appropriate target segment.

We use our own patch table rather than emitting DOS MZ .EXE relocations for three reasons:

  1. Allowing boot-loaded mode where no MZ loader is used.
  2. Allowing the debugger to be compressed without parsing relocations into the depacker.
  3. Allowing to dynamically determine the layout and placement of the code segments at run time instead of leaving them fixed relative to the PSP.

In the DPMI-capable lDebugX, the dualcall macro will instead always call a helper function. Each code segment has two such helpers: One to call a function in the same segment, and one to do an actual inter-segment function call. For both, the target offset is stored behind the (near) call instruction in the caller's code. The helper will construct a stack frame so that it can branch to the desired target function, either near or far. For true inter-segment calls this far branch always uses an actual segment or selector, chosen by the helper thunk to match the current mode. The target function will receive a stack frame consisting of a far-like return address, as well as the stack as set up by the original caller.

A dual function must return with a call to the dualreturn helper, then a retf or retf imm16 instruction. Further, dual functions have to use far call stack frames. To simplify this, the macros define dualdistance to be far if dual code segments are in use and near if not. Dual functions generally should use lframe dualdistance to set up their stack frames. Then any number of lpar and a use of lpar_return are permitted. The dual function can directly access parameters on its stack then.

The nature of far-like return addresses is that they take up 32 bits, just like actual far return addresses. However, the segment/selector value in the high word is instead used to store an index. The dual return helper examines the index and replaces it by the desired segment or selector to which to return. This complication allows any function to switch modes (from Real/Virtual 86 Mode to Protected Mode, or vice versa). The possible need for this was determined important enough to support such uses, although no current users are known. The index supports both modes. An actual segment or selector value is inserted to replace the index only in the dual return helper, which must be immediately followed by the far return instruction that will use this far return address.

Finally, the section_of and section_of_function macros are used to tell the macros which section a target function belongs to. section_of for a specific symbol currently must be used before any nearcall or dualcall macro that uses that symbol. The section to note down is the current active section as selected by the addsection or usesection macros (from lmacros3.mac). The section_of_function macro is used at a function's definition to verify that the correct section was specified.

section_of may be used even when the specified function does not exist, for example when it is not included due to the current build options. A label specified with the section_of_function macro, however, must correspond to an earlier use of section_of.

Examples

This example is based on the initial revision that introduced the dual code sections support. This is what the bu_relocated function would look like as a normal, near-callable function within the single lDEBUG_CODE section:

bu_relocated:
	lframe near
	lpar word, sign
	lenter
	mov ax, word [bp + ?sign]
	mov di, msg.bu_relocated.sign
	call hexword
	mov dx, msg.bu_relocated
	call putsz
	lleave
	lret

Note that this hardcodes a near stack frame. And this is how it would be called:

	mov ax, 2642h
	 push ax
	call bu_relocated

Next, here's how to change it to a dual-callable function which goes into the lDEBUG_CODE2 section if that is used:

%if _DUALCODE
	usesection lDEBUG_CODE2
%endif

section_of bu_relocated
dualfunction
bu_relocated:
	lframe dualdistance
	lpar word, sign
	lenter
	mov ax, word [bp + ?sign]
	mov di, msg.bu_relocated.sign
	nearcall hexword
	mov dx, msg.bu_relocated
	nearcall putsz
	lleave
	dualreturn
	lret

And this is a caller:

	mov ax, 2642h
	 push ax
	dualcall bu_relocated

Note the uses of dualdistance and dualreturn. When this source is compiled with dual code segments enabled, the distance of the stack frame will be far instead. If the build is DPMI-capable then the dualcall helper function will push a far-like return address, which the dualreturn helper will then convert into a segmented far return address. The lret will expand to a retf 2 in this case.

Converting function calls to use the nearcall method only involves replacing plain call instructions by the nearcall macro invocation, as well as specifying the correct sections with section_of. As mentioned, a function must not use lpar parameters on its stack in order to call it using nearcall.

Discussion

C. MaslochC. Masloch, 2022-09-04 16:10:45 +0200 Sep Sun

I just added a new option: _DUALCODENEARDUAL. When enabled, and _DUALCODE=1 _PM=0, then uses of the nearcall macro which emit inter-segment calls will simply invoke the dualcall macro. The target of that dualcall is a helper in the target segment, which calls the target function (near) and then returns far. This helper is written once for each function referenced by a nearcall from the other segment. The normal nearcall helper functions (one in each segment) are not used then and consequently omitted from the binary.

This option only takes effect if not building with DPMI support. This is because dualcall requires a helper function call regardless for the DPMI builds, so there's little to be saved by replacing a nearcall by a dualcall.

You could leave a comment if you were logged in.
blog/pushbx/2022/0826_dual_code_segments_mechanisms.txt · Last modified: 2022-08-26 21:37:33 +0200 Aug Fri by ecm