Like user3840170 commented, the 21.4B01 call will change the current PSP to the newly created process. However, you should not free its memory on your own. Even if you change the current PSP back to another process (such as your own), you may still leak resources. The most obvious example is file handles, including in the System File Tables which are shared among all processes in the system. In some use cases, such as using redirection like
> NUL, you may leak SFT entries entirely for the remaining uptime of the system. (The default file handles such as stdin/stdout/stderr will usually refer to a shared CON entry in the SFTs so not as bad then.)
(Writing of which, if you use service 21.31 for TSR you should probably free all file handles and the environment memory block too before calling the TSR termination, unless you know specifically that you want to use them afterwards. If your TSR never switches the system's current PSP to its own then you do not need these handles left over opened.)
Here's some example code from the FreeDOS Debug descendant lDebug: https://hg.pushbx.org/ecm/ldebug/file/7bd51f7b763f/source/ll.asm#l362 I maintain this project, though I do not believe that I modified this particular part much in my fork.
; Actual program loading. Use the DOS interrupt.
mov ax, 4B01h ; load program
mov dx, DTA ; offset of file to load
mov bx, execblk ; parameter block
int 21h ; load it
jc ll16 ; if error
If this call returns an error then it will not have changed to a new process.
mov ax, sp
sub ax, [SPSAV]
cmp ax, 80h
jb ll15 ; if in range
mov ax, 80h
mov word [spadjust], ax
It is crucial to understand that what is called SPSAV here (the
dword [PSP:2Eh]) is set by DOS on most if not all interrupt 21h calls (certainly 21.4B00 and .4B01). The singular purpose of it is to point a DOS process termination (services 4Ch or 31h, or older variants, or Ctrl-C or Critical Error Abort) back to the parent process's stack when the control flow returns to the parent. To utilise it, DOS will read the SPSAV field in the parent's PSP.
Now the debugger plays games with this in https://hg.pushbx.org/ecm/ldebug/file/7bd51f7b763f/source/run.asm#l5372 because it doesn't want DOS, upon the debuggee's termination, to overwrite the stack used by the debugger while it is in a run command. This does mean none of the registers other than
ss:sp have any known contents when the control flow returns to the debugger. (The flags should also be forced valid by DOS; the FreeDOS kernel used to have a bug about that. The flags should be NC and TF=0.) If DOS returns to a normal child process created by 21.4B00 then the DOS entry stack will still be valid and DOS will return the 16-bit registers to their prior state.
les si, [execblk.sssp]
es lodsw ; recover ax
mov word [reg_eax], ax
mov word [reg_esp], si
mov word [reg_ss], es
les si, [execblk.csip]
mov word [reg_eip], si
mov word [reg_cs], es
This retrieves the official initial register values from the service's exec parameter block. They will be loaded into the actual registers by the debugger once a run command traces or goes to execute the debuggee.
xchg ax, bx ; ax = PSP, clobber bx
mov word [pspdbe], ax
This retrieves the current PSP and stores it away for the debugger to use, which is also used for the run commands to switch to that PSP again later.
clropt [internalflags], attachedterm
mov di, reg_ds
stosw ; reg_es
This initialises the
es registers which are documented as pointing to the PSP upon process start (even if an MZ .exe program's
ss possibly do not point to the PSP).
This restores the debugger's PSP as the current one. To a lesser extent than the SPSAV handling, the debugger is playing games: It wants to run its own code in its process, but run the debuggee code in the newly created process. This is relevant for which std handles are used, and which process is used for any memory block allocations or resizes, or opening additional files.
; Finish up. Set termination address.
mov ax, 2522h ; set interrupt vector 22h
mov dx, int22 ; ds => lDEBUG_DATA_ENTRY
mov word [TPIV], dx
mov word [TPIV+2], ss ; => lDEBUG_DATA_ENTRY
This is the most important part: Modify the Parent Return Address of the newly created process. This is also known as interrupt 22h or the TPIV = Terminate Program Interrupt Vector. What's important to understand is that it is pure formality to call it an interrupt vector, and the 21.2522 call here is in fact not needed. It just happens to be the case that DOS stores the PRA just before the saved original int 23h and int 24h handlers in a child PSP. There is never an actual call to interrupt 22h (
int 22h instruction). DOS simply finds the
cs:ip at which to return to the parent by reading the PRA field in the child process that is being terminated. It will insert this return address into the
cs:ip field of the stack frame it creates to return to the parent process. (As mentioned, this stack frame's position is found from the SPSAV field of the parent process.)
That means usually the PRA is initialised to the very next address behind the
int 21h instruction that created the child process using function 4B00h or 4B01h. For 4B00h, the PRA is usually not modified and DOS will return there only once the child process terminates. At termination it will read the PRA from the child PSP to remember where to return to. It will set up the stack frame, force its
cs:ip to the PRA, and set the flags to a valid value. This has to give NC (Carry Flag zero) in order to indicate to the parent process that the exec call succeeded.
With the debugger load and do not execute function 4B01h, DOS will actually return twice to the same address if not otherwise instructed. There is no standard way to detect whether a return to that address is after the service returned having created the child process, or after the termination of the child process made DOS go back here.
So the debugger plays more games: It sets up the entrypoint here called
int22 as the new PRA, as a 16:16 segmented far address. (lDebug has a code segment that is at a different address than its data/entry segment.) Then the debugger can be certain that if it is entered in its "interrupt 22h" handler then this is actually the return from the process termination.
This answer would not be complete without mentioning that you can do more than this if you go to lengths to modify the PRA and parent process fields of processes: You can return to a different process than your original parent. In fact, the debugger and the DOS shell set themselves up as their own parent processes. DOS knows to check for this and will skip freeing memory and file handles if a self-parented process terminates. (Whether by 21.4C or Ctrl-C or Critical Error Abort. 21.31 never frees these resources even when normally returning to a different parent process.) It is important to modify not only the parent field in the PSP but also the PRA so our code gains control again after the termination. Also, the SPSAV field in "the parent" is used again for finding the stack frame to use.
Another bit is function 21.4B05 which I believe should actually be called by the debugger to set up the genuine exec state for debuggees. We didn't have a use case for it yet, though. In particular, I think function 21.4B00 usually disables the A20 gate upon execution of a program, which 21.4B01 might not do. A20 off is needed for some misbehaved programs that assume an address space wrap at 1 MiB.
There is also the fact that the old style termination functions, interrupts 20h, 27h, and 21h function 00h, may actually use the caller's
cs instead of the current PSP to determine what process is terminating. (FreeDOS does not replicate this I think.) I believe the free software sources of MS-DOS version 2 contain some of these differences, if not all of them. The Microsoft Debug program relies on this oddity for its proper operation.
So, briefly: DOS creates a new process. Modify your process's SPSAV and the child process's PRA accordingly to return the control flow to your program upon termination of the child. Then simply set up the stack, and
es = PSP, of the child program and do a far branch (far jump or retf or iret) to the child program's entrypoint, as indicated in the fields written by DOS in the exec parameter block. Everything will work out as intended when and if the child process terminates.