diff --git a/djgpp/makefile b/djgpp/makefile index 9d10bd6..d53554a 100644 --- a/djgpp/makefile +++ b/djgpp/makefile @@ -12,7 +12,7 @@ C_OPT = -Wall -O2 -Wmissing-declarations -Wwrite-strings -I $(SRCDIR) -I . \ # avoid CMOVxx instructions C_OPT += -march=i386 LINK_OPT = -SRCS = $(SRCDIR)/command.c $(SRCDIR)/cmdbuf.c $(SRCDIR)/ms.c $(SRCDIR)/djterm.c \ +SRCS = $(SRCDIR)/command.c $(SRCDIR)/cmdbuf.c $(SRCDIR)/ms.c $(SRCDIR)/djterm.c $(SRCDIR)/mem.c \ $(SRCDIR)/env.c $(SRCDIR)/psp.c $(SRCDIR)/umb.c $(SRCDIR)/ae0x.c \ $(SRCDIR)/compl.c $(SRCDIR)/clip.c memmem.c fmemcpy.c findclos.c ASSRCS = $(SRCDIR)/asm.S $(SRCDIR)/int23.S $(SRCDIR)/int0.S $(SRCDIR)/int75.S \ diff --git a/src/command.c b/src/command.c index 25884bd..1c498e4 100644 --- a/src/command.c +++ b/src/command.c @@ -97,6 +97,7 @@ #include "ae0x.h" #include "compl.h" #include "clip.h" +#include "mem.h" #include "command.h" /* @@ -305,6 +306,7 @@ struct built_in_cmd cmd_table[] = {"loadhigh", perform_loadhigh, "", "load program to UMB"}, {"md", perform_md, "", "create directory"}, {"mkdir", perform_md, "", "create directory"}, + {"mem", perform_mem, " [/c] [/f] [/d] [/p]", "display memory statistics"}, {"move", perform_move, "", "move file"}, {"more", perform_more, "", "scroll-pause long output"}, {"mouseopt", perform_mouseopt, "", "mouse options"}, @@ -412,7 +414,12 @@ static int has_wildcard(char *s) return false; } -static void reset_batfile_call_stack(void) +const char *get_cmd_args(void) + { + return cmd_args; + } + +void reset_batfile_call_stack(void) { static int first_time = true; int ba; diff --git a/src/command.h b/src/command.h index e416a33..064db53 100644 --- a/src/command.h +++ b/src/command.h @@ -475,6 +475,11 @@ struct built_in_cmd extern struct built_in_cmd cmd_table[]; extern const int CMD_TABLE_COUNT; +extern void reset_batfile_call_stack(void); +/* full argument string of the command being executed: built-in commands + * are handed only the first argument, so anything that takes more than + * one switch needs this */ +extern const char *get_cmd_args(void); unsigned short keyb_get_shift_states(void); diff --git a/src/makefile.inc b/src/makefile.inc index edb366c..0ad30eb 100644 --- a/src/makefile.inc +++ b/src/makefile.inc @@ -16,10 +16,10 @@ DJCFLAGS = -Wall -O2 -Wmissing-declarations -Wwrite-strings \ DJCPPFLAGS += -I. -I$(srcdir) -DCOMCOM_VERSION=\"$(VERSION)\" DJASFLAGS += -I. -I$(srcdir) DJASCPPFLAGS += -I. -I$(srcdir) -SOURCES = command.c cmdbuf.c mouse.c env.c psp.c umb.c ae0x.c compl.c clip.c \ +SOURCES = command.c cmdbuf.c mouse.c env.c psp.c umb.c ae0x.c compl.c clip.c mem.c \ djterm.c thunks_a.c thunks_c.c HEADERS = $(addprefix $(srcdir)/,ae0x.h cmdbuf.h compl.h psp.h command.h env.h mouse.h umb.h \ - glob_asm.h asm.h) + mem.h glob_asm.h asm.h) PDHDR = $(srcdir)/asm.h GLOB_ASM = $(srcdir)/glob_asm.h OBJECTS = $(SOURCES:.c=.o) diff --git a/src/mem.c b/src/mem.c new file mode 100644 index 0000000..2138c79 --- /dev/null +++ b/src/mem.c @@ -0,0 +1,958 @@ +/* + * comcom64 - 64bit command.com + * mem.c: FreeDOS-like MEM command implementation + * Copyright (C) 2026 @stsp/JulesAI, @stuaxo/Claude + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "asm.h" +#include "command.h" +#include "mem.h" + +/* + * Everything here is accounted for, and displayed, in KB - the same unit + * real FreeDOS MEM reports. Each source of numbers is converted to KB + * exactly once, right where it is read: INT 12h and XMS/INT 15h already + * speak KB, EMS counts 16KB pages, and the MCB chain counts 16-byte + * paragraphs. Nothing is converted from KB to bytes and back again on + * its way to the screen. + */ +#define PARAS_PER_KB 64 +#define EMS_PAGE_KB 16 + +/* + * Conventional memory ends at the 640K line - segment 0xA000 - and + * everything at or above it is upper memory. Nothing we report as + * conventional may exceed that, whatever the BIOS or the MCB chain + * says. + */ +#define UMB_START_SEG 0xA000 +#define CONV_MAX_KB (UMB_START_SEG / PARAS_PER_KB) + +/* + * Above conventional memory, FreeDOS MEM splits the first megabyte the + * way MS-DOS MEM did: 0xA000-0xDFFF is "Upper", and 0xE000-0xFFFF is + * "Reserved" - the BIOS and option ROM area, reported as entirely in + * use. Both are fixed-size regions, not sums of the blocks found in + * them, and free UMBs count towards upper memory wherever they sit. + * Report them the same way, so our numbers can be compared with + * MEM.EXE's directly. + */ +#define RESERVED_START_SEG 0xE000 +#define UMB_TOTAL_KB ((RESERVED_START_SEG - UMB_START_SEG) / PARAS_PER_KB) +#define RESERVED_TOTAL_KB ((0x10000 - RESERVED_START_SEG) / PARAS_PER_KB) + +/* Rounds to nearest, as FreeDOS MEM does. */ +static uint32_t paras_to_kb(uint32_t paras) +{ + return (paras + PARAS_PER_KB / 2) / PARAS_PER_KB; +} + +struct MCB { + char id; /* 'M' (0x4D) or 'Z' (0x5A) */ + uint16_t owner_psp; /* 0 = free, 8 = system, else PSP segment */ + uint16_t size; /* Size in 16-byte paragraphs */ + char reserved[3]; + char name[8]; /* Owner/program name in DOS 4+ */ +} __attribute__((packed)); + +struct module_entry { + uint16_t owner_psp; + char name[16]; /* matches get_mcb_name()'s name_out buffer size */ + uint32_t conv_paras; + uint32_t umb_paras; +}; + +static int opt_classify = 0; +static int opt_free = 0; +static int opt_debug = 0; +static int opt_page = 0; +static int page_bottom = 0; +static int page_stop = 0; + +static void reset_page(void) +{ + struct text_info txinfo; + + page_stop = 0; + gettextinfo(&txinfo); + page_bottom = txinfo.winbottom; + /* start the report on a screen of its own, as "dir /p" does, so the + * first page is a full one wherever the command line left the cursor */ + if (opt_page) + clrscr(); +} + +/* + * Pause on reaching the last line of the window, rather than counting + * the lines printed so far: our output starts wherever the command line + * left the cursor and long lines wrap, so a counter that starts at zero + * pauses in the wrong place - or, for the short default report, never + * pauses at all while the earlier lines scroll away. This is what + * "dir /p" does, so /PAGE now behaves the same in both commands. + */ +static void print_line(const char *str) +{ + int c; + + if (page_stop) + return; + printf("%s\n", str); + if (!opt_page) + return; + + /* the cursor only moves once stdout reaches the screen */ + fflush(stdout); + if (wherey() < page_bottom) + return; + + printf("Press any key to continue, or q to stop..."); + fflush(stdout); + c = getch(); + if (c == 3 || c == 27 || toupper(c) == 'Q') + { + printf("\n"); + page_stop = 1; + return; + } + clrscr(); +} + +static void format_num(uint32_t val, char *buf, size_t buf_size) +{ + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%lu", (unsigned long)val); + int len = strlen(tmp); + int dest_idx = 0; + int i; + + for (i = 0; i < len; i++) + { + if (i > 0 && (len - i) % 3 == 0) + { + if (dest_idx < (int)buf_size - 1) + buf[dest_idx++] = ','; + } + if (dest_idx < (int)buf_size - 1) + buf[dest_idx++] = tmp[i]; + } + buf[dest_idx] = '\0'; +} + +static void format_kb(uint32_t kb, char *buf, size_t buf_size) +{ + size_t len; + + format_num(kb, buf, buf_size); + len = strlen(buf); + if (len < buf_size - 1) + { + buf[len] = 'K'; + buf[len + 1] = '\0'; + } +} + +/* + * MEM.EXE prints the largest blocks and the EMS totals in KB with the + * byte count next to it, and the dosemu2 test suite parses that byte + * count, so print both. This is the only place a KB figure is turned + * back into bytes, and only for display. + */ +static void print_kb_and_bytes(const char *label, uint32_t kb, uint32_t bytes) +{ + char kb_str[24], b_str[24], buf[160]; + + format_kb(kb, kb_str, sizeof(kb_str)); + format_num(bytes, b_str, sizeof(b_str)); + snprintf(buf, sizeof(buf), "%-36s%8s (%s bytes)", label, kb_str, b_str); + print_line(buf); +} + +struct psp_name { + uint16_t psp; + char name[9]; +}; + +#define MAX_PSP_NAMES 64 + +/* Copies the MCB's owner name out, if DOS put a usable one there. */ +static int copy_mcb_name(const struct MCB *mcb, char *out, size_t out_size) +{ + int i; + + for (i = 0; i < 8; i++) + { + if (mcb->name[i] == 0 || mcb->name[i] == ' ') + break; + if (!isprint((unsigned char)mcb->name[i]) || (size_t)i >= out_size - 1) + return 0; + out[i] = mcb->name[i]; + } + out[i] = '\0'; + return i > 0; +} + +/* + * DOS 4+ stores the owner's name in the MCB of the block holding the + * program itself - the one whose PSP is its first paragraph. A + * program's other blocks (its environment, anything it allocates later) + * carry no usable name, so collect the names up front and then look + * them up by owner. Naming each block on its own instead is what made + * MEM /CLASSIFY list one module twice, once as COMMAND and once as + * PSP-xxxx. + */ +static int collect_psp_names(uint16_t first_mcb_seg, struct psp_name *names, + int max_names) +{ + uint16_t seg = first_mcb_seg; + int num = 0; + + while (seg != 0 && num < max_names) + { + struct MCB mcb; + + dosmemget(seg << 4, sizeof(mcb), &mcb); + if (mcb.id != 'M' && mcb.id != 'Z') + break; + + if (mcb.owner_psp == seg + 1 && + copy_mcb_name(&mcb, names[num].name, sizeof(names[num].name))) + names[num++].psp = mcb.owner_psp; + + if (mcb.id == 'Z') + break; + if (seg + 1 + mcb.size > 0xffff) + break; + seg += 1 + mcb.size; + } + return num; +} + +/* + * DOS keeps the upper memory chain linked across the areas that are not + * RAM at all - the video memory and the option ROMs - with placeholder + * blocks owned by the kernel and named "SC". They are holes in the + * address space rather than memory in use, and neither MEM.EXE nor + * dosemu2's own debugger counts them, so skip them too. + */ +static int mcb_is_link(const struct MCB *mcb) +{ + char name[9]; + + return mcb->owner_psp == 8 && copy_mcb_name(mcb, name, sizeof(name)) && + strcmp(name, "SC") == 0; +} + +static void get_mcb_name(const struct MCB *mcb, const struct psp_name *names, + int num_names, char *name_out, size_t name_out_size) +{ + int i; + + memset(name_out, 0, name_out_size); + + if (mcb->owner_psp == 0) + { + strncpy(name_out, "Free", name_out_size - 1); + return; + } + + /* + * Blocks owned by DOS carry the name of the driver loaded into them, + * where there is one - that is how MEM.EXE tells a driver apart from + * the kernel's own memory. "SC" and "SD" are not drivers though, they + * are the kernel's own markers for its link blocks and its data. + */ + if (mcb->owner_psp == 8) + { + if (mcb_is_link(mcb)) + strncpy(name_out, "LINK", name_out_size - 1); + else if (!copy_mcb_name(mcb, name_out, name_out_size) || + strcmp(name_out, "SD") == 0) + strncpy(name_out, "SYSTEM", name_out_size - 1); + return; + } + + for (i = 0; i < num_names; i++) + { + if (names[i].psp == mcb->owner_psp) + { + strncpy(name_out, names[i].name, name_out_size - 1); + return; + } + } + + if (copy_mcb_name(mcb, name_out, name_out_size)) + return; + + /* Try extracting program name from owner PSP environment block */ + uint16_t env_seg = _farpeekw(_dos_ds, (mcb->owner_psp << 4) + 0x2c); + if (env_seg != 0) + { + uint32_t env_addr = env_seg << 4; + uint32_t offset = 0; + uint8_t b1 = _farpeekb(_dos_ds, env_addr + offset); + uint8_t b2 = _farpeekb(_dos_ds, env_addr + offset + 1); + + while (offset < 0x7fff) + { + if (b1 == 0 && b2 == 0) + { + offset += 2; + uint16_t count = _farpeekw(_dos_ds, env_addr + offset); + if (count == 1) + { + offset += 2; + char path[128]; + int p_idx = 0; + while (p_idx < sizeof(path) - 1) + { + char ch = _farpeekb(_dos_ds, env_addr + offset + p_idx); + if (ch == 0) + break; + path[p_idx++] = ch; + } + path[p_idx] = '\0'; + + char *fname = strrchr(path, '\\'); + if (!fname) + fname = strrchr(path, '/'); + if (!fname) + fname = path; + else + fname++; + + char *dot = strchr(fname, '.'); + if (dot) + *dot = '\0'; + + if (strlen(fname) > 0) + { + strncpy(name_out, fname, name_out_size - 1); + return; + } + } + break; + } + offset++; + b1 = b2; + b2 = _farpeekb(_dos_ds, env_addr + offset + 1); + } + } + + snprintf(name_out, name_out_size, "PSP-%04X", mcb->owner_psp); +} + +struct e820_entry { + uint64_t base; + uint64_t length; + uint32_t type; + uint32_t ext_attr; +} __attribute__((packed)); + +#define E820_TYPE_USABLE 1 +#define E820_MAX_ENTRIES 64 + +/* + * XMS only ever reports free extended memory (function 08h gives the + * largest free block and total free, nothing else) - there is no XMS + * call for total installed extended memory, so total has to come from + * the BIOS. Prefer the INT 15h/E820h system memory map, same as real + * FreeDOS's own MEM: under dosemu2 the older E801h/88h calls can report + * a much smaller "compatibility" figure than the extended memory pool + * that's actually configured, while E820h reflects it accurately. Fall + * back to E801h (covers configurations above 64MB) and then the even + * older AH=88h (limited to 64MB) only if E820h isn't supported. + */ +static uint32_t query_ext_mem_total_kb(void) +{ + uint32_t total_kb = 0; + uint32_t continuation = 0; + int i; + + for (i = 0; i < E820_MAX_ENTRIES; i++) + { + struct e820_entry entry = {}; + __dpmi_regs r = {}; + + r.d.eax = 0xe820; + r.d.edx = 0x534d4150; /* 'SMAP' */ + r.d.ecx = sizeof(entry); + r.d.ebx = continuation; + r.x.es = __tb_segment; + r.d.edi = __tb_offset; + __dpmi_int(0x15, &r); + + if ((r.x.flags & 1) || r.d.eax != 0x534d4150 || r.d.ecx == 0) + break; + + dosmemget(__tb, r.d.ecx < sizeof(entry) ? r.d.ecx : sizeof(entry), &entry); + + if (entry.type == E820_TYPE_USABLE && entry.base >= 0x100000) + total_kb += (uint32_t)(entry.length / 1024); + + continuation = r.d.ebx; + if (continuation == 0) + break; + } + + if (total_kb > 0) + return total_kb; + + __dpmi_regs r = {}; + r.x.ax = 0xe801; + __dpmi_int(0x15, &r); + if (!(r.x.flags & 1)) + { + uint32_t below_16m = r.x.ax ? r.x.ax : r.x.cx; + uint32_t above_16m_64k_blocks = r.x.ax ? r.x.bx : r.x.dx; + uint32_t total = below_16m + above_16m_64k_blocks * 64; + if (total > 0) + return total; + } + + __dpmi_regs r2 = {}; + r2.h.ah = 0x88; + __dpmi_int(0x15, &r2); + if (!(r2.x.flags & 1) && r2.x.ax > 0) + return r2.x.ax; + + return 0; +} + +static void show_help(void) +{ + reset_page(); + print_line("Displays the amount of used and free memory in your system."); + print_line(""); + print_line("MEM [/CLASSIFY | /FREE | /DEBUG] [/PAGE]"); + print_line(""); + print_line(" /CLASSIFY or /C Classifies programs by memory usage."); + print_line(" /FREE or /F Lists free memory blocks."); + print_line(" /DEBUG or /D Displays detailed memory listing."); + print_line(" /PAGE or /P Pauses after each screen of information."); +} + +void perform_mem(const char *arg) +{ + /* + * Built-in commands are handed only the first argument, while MEM + * takes a combination of switches - "mem /d /p" used to arrive here + * as just "/d", silently dropping /PAGE - so parse the full argument + * string instead. + */ + const char *p = get_cmd_args(); + + opt_classify = 0; + opt_free = 0; + opt_debug = 0; + opt_page = 0; + + while (*p) + { + while (isspace((unsigned char)*p)) + p++; + if (!*p) + break; + if (*p == '/' || *p == '-') + { + p++; + if (*p == '?') + { + show_help(); + return; + } + + const char *tok = p; + while (isalnum((unsigned char)*p)) + p++; + size_t tok_len = p - tok; + + if ((tok_len == 1 && strnicmp(tok, "C", 1) == 0) || + (tok_len == 8 && strnicmp(tok, "CLASSIFY", 8) == 0)) + opt_classify = 1; + else if ((tok_len == 1 && strnicmp(tok, "F", 1) == 0) || + (tok_len == 4 && strnicmp(tok, "FREE", 4) == 0)) + opt_free = 1; + else if ((tok_len == 1 && strnicmp(tok, "D", 1) == 0) || + (tok_len == 5 && strnicmp(tok, "DEBUG", 5) == 0)) + opt_debug = 1; + else if ((tok_len == 1 && strnicmp(tok, "P", 1) == 0) || + (tok_len == 4 && strnicmp(tok, "PAGE", 4) == 0)) + opt_page = 1; + else if (tok_len == 4 && strnicmp(tok, "HELP", 4) == 0) + { + show_help(); + return; + } + else + { + cprintf("Invalid switch - %s\r\n", tok - 1); + reset_batfile_call_stack(); + return; + } + } + else + { + /* not a switch - skip the whole word, not just one character */ + while (*p && !isspace((unsigned char)*p)) + p++; + } + } + + reset_page(); + + /* + * Save UMB link state and link UMBs for the memory scan. This talks + * to INT 21h/58h(03h) (Set UMB Link State) directly instead of going + * through link_umb()/unlink_umb() in umb.c: those also poke the + * allocation strategy via 58h(01h), and unlink_umb() hardcodes it + * back to 0 rather than whatever it was before MEM ran. Doing the + * link/unlink here means MEM never touches allocation strategy at + * all, so there's nothing to restore. + */ + __dpmi_regs r = {}; + r.x.ax = 0x5802; + __dpmi_int(0x21, &r); + uint8_t orig_umblink = r.h.al; + + r.x.ax = 0x5803; + r.x.bx = 1; + __dpmi_int(0x21, &r); + + /* Get List of Lists -> First MCB */ + r.h.ah = 0x52; + __dpmi_int(0x21, &r); + uint32_t sysvars_addr = (r.x.es << 4) + r.x.bx; + uint16_t first_mcb_seg = _farpeekw(_dos_ds, sysvars_addr - 2); + + uint32_t conv_total_kb = 0; + uint32_t conv_total_paras = 0; + uint32_t conv_free_paras = 0; + uint32_t conv_largest_paras = 0; + + uint32_t umb_free_paras = 0; + uint32_t umb_largest_paras = 0; + + /* runs of adjacent free blocks, see below */ + uint32_t conv_run_paras = 0; + uint32_t umb_run_paras = 0; + + struct module_entry modules[64]; + int num_modules = 0; + struct psp_name psp_names[MAX_PSP_NAMES]; + int num_psp_names; + + memset(modules, 0, sizeof(modules)); + memset(psp_names, 0, sizeof(psp_names)); + num_psp_names = collect_psp_names(first_mcb_seg, psp_names, MAX_PSP_NAMES); + + /* + * Memory below the first MCB - the interrupt vector table, the BIOS + * data area and the resident kernel - belongs to no block, but it is + * in use, and MEM.EXE counts it towards SYSTEM. Start the module list + * with it, so the listing adds up to the conventional total and opens + * with SYSTEM, as MEM.EXE's does. + */ + if (opt_classify) + { + modules[0].owner_psp = 8; + strcpy(modules[0].name, "SYSTEM"); + modules[0].conv_paras = first_mcb_seg; + num_modules = 1; + } + + char line_buf[160]; + + if (opt_debug) + { + print_line("Memory Detail:"); + print_line(" Segment Owner Size Type/Name"); + print_line(" ------- ------- -------- -----------"); + } + else if (opt_free) + { + print_line("Free Memory Blocks:"); + print_line(" Segment Size Memory Type"); + print_line(" ------- -------- -----------"); + } + + uint16_t curr_seg = first_mcb_seg; + while (curr_seg != 0) + { + struct MCB mcb; + dosmemget(curr_seg << 4, sizeof(mcb), &mcb); + + if (mcb.id != 'M' && mcb.id != 'Z') + break; + + uint32_t block_paras = mcb.size; + int is_umb = (curr_seg >= UMB_START_SEG); + int is_link = mcb_is_link(&mcb); + + /* + * A block can cross the 640K line: with UMBs linked in, DOS merges + * the top of conventional memory with an adjacent upper memory + * block into a single MCB. Count each half against the region it + * lies in, or conventional memory ends up bigger than 640K. + */ + uint32_t conv_paras = 0; + uint32_t umb_paras = 0; + + if (is_umb) + umb_paras = block_paras; + else if (curr_seg + block_paras <= UMB_START_SEG) + conv_paras = block_paras; + else + { + conv_paras = UMB_START_SEG - curr_seg; + umb_paras = block_paras - conv_paras; + } + + if (is_link) + { + /* a hole in the address space: listed, but not counted */ + conv_paras = 0; + umb_paras = 0; + } + + char name[16]; + get_mcb_name(&mcb, psp_names, num_psp_names, name, sizeof(name)); + + /* the MCB header itself belongs to the region it sits in */ + if (!is_umb) + conv_total_paras += conv_paras + 1; + + if (mcb.owner_psp == 0) + { + conv_free_paras += conv_paras; + umb_free_paras += umb_paras; + + /* + * DOS merges adjacent free blocks when it allocates, so a run of + * them is a single allocatable block - and the MCB headers + * between them become usable too. Tracking runs rather than + * single blocks is what makes the largest sizes agree with what + * MEM.EXE reports: right after a program exits, the memory it + * used is several adjacent free blocks that DOS has not merged + * yet. + */ + if (conv_paras) + { + conv_run_paras = conv_run_paras ? conv_run_paras + 1 + conv_paras : + conv_paras; + if (conv_run_paras > conv_largest_paras) + conv_largest_paras = conv_run_paras; + } + else + conv_run_paras = 0; + + if (umb_paras) + { + umb_run_paras = umb_run_paras ? umb_run_paras + 1 + umb_paras : + umb_paras; + if (umb_run_paras > umb_largest_paras) + umb_largest_paras = umb_run_paras; + } + else + umb_run_paras = 0; + } + else + { + conv_run_paras = 0; + umb_run_paras = 0; + } + + /* Collect module info for /CLASSIFY */ + if (opt_classify && !is_link) + { + int found = -1; + int i; + for (i = 0; i < num_modules; i++) + { + if (modules[i].owner_psp == mcb.owner_psp && strcmp(modules[i].name, name) == 0) + { + found = i; + break; + } + } + if (found == -1 && num_modules < 64) + { + found = num_modules++; + modules[found].owner_psp = mcb.owner_psp; + strncpy(modules[found].name, name, sizeof(modules[found].name) - 1); + } + if (found >= 0) + { + if (is_umb) + modules[found].umb_paras += umb_paras + 1; + else + { + modules[found].conv_paras += conv_paras + 1; + modules[found].umb_paras += umb_paras; + } + } + } + + if (opt_debug) + { + char sz_str[24]; + format_kb(paras_to_kb(block_paras), sz_str, sizeof(sz_str)); + snprintf(line_buf, sizeof(line_buf), " %04Xh %04Xh %8s %s", + curr_seg, mcb.owner_psp, sz_str, name); + print_line(line_buf); + } + else if (opt_free && mcb.owner_psp == 0) + { + char sz_str[24]; + format_kb(paras_to_kb(block_paras), sz_str, sizeof(sz_str)); + snprintf(line_buf, sizeof(line_buf), " %04Xh %8s %s", + curr_seg, sz_str, is_umb ? "Upper" : "Conventional"); + print_line(line_buf); + } + + if (mcb.id == 'Z') + break; + + if (curr_seg + 1 + mcb.size > 0xffff) + break; /* corrupt chain: don't wrap around into low memory */ + curr_seg += 1 + mcb.size; + } + + if (!orig_umblink) + { + r.x.ax = 0x5803; + r.x.bx = 0; + __dpmi_int(0x21, &r); + } + + if (page_stop) + return; + + /* + * The MCB arena walked above starts after the resident DOS kernel, + * interrupt vector table and BIOS data area, so its sum is a few KB + * short of the true installed conventional memory. INT 12h reports + * the BIOS-detected total in KB directly, which is what real MEM.COM + * shows as "Total conventional memory". + */ + __dpmi_regs ir = {}; + __dpmi_int(0x12, &ir); + if (ir.x.ax > 0) + conv_total_kb = ir.x.ax; + else + conv_total_kb = conv_total_paras ? paras_to_kb(conv_total_paras) : CONV_MAX_KB; + if (conv_total_kb > CONV_MAX_KB) + conv_total_kb = CONV_MAX_KB; + + uint32_t conv_free_kb = paras_to_kb(conv_free_paras); + uint32_t conv_used_kb = (conv_total_kb > conv_free_kb) ? + (conv_total_kb - conv_free_kb) : 0; + uint32_t umb_total_kb = UMB_TOTAL_KB; + uint32_t umb_free_kb = paras_to_kb(umb_free_paras); + uint32_t umb_used_kb = (umb_total_kb > umb_free_kb) ? + (umb_total_kb - umb_free_kb) : 0; + + if (opt_classify) + { + print_line(""); + print_line("Modules using memory below 1 MB:"); + print_line(""); + print_line(" Name Total Conventional Upper Memory"); + print_line(" -------- -------- ------------ ------------"); + + /* loaded modules in the order they sit in memory, free memory last, + * the way MEM.EXE lists them */ + int pass, i; + for (pass = 0; pass < 2; pass++) + { + for (i = 0; i < num_modules; i++) + { + char t_str[24], c_str[24], u_str[24]; + int is_free = (modules[i].owner_psp == 0); + + if (is_free != pass) + continue; + + format_kb(paras_to_kb(modules[i].conv_paras + modules[i].umb_paras), + t_str, sizeof(t_str)); + format_kb(paras_to_kb(modules[i].conv_paras), c_str, sizeof(c_str)); + format_kb(paras_to_kb(modules[i].umb_paras), u_str, sizeof(u_str)); + + snprintf(line_buf, sizeof(line_buf), " %-8s %8s %12s %12s", + modules[i].name, t_str, c_str, u_str); + print_line(line_buf); + } + } + print_line(""); + } + + /* Query XMS */ + uint32_t xms_free_kb = 0; + int xms_present = 0; + + __dpmi_regs dr = {}; + dr.x.ax = 0x4300; + __dpmi_int(0x2f, &dr); + if ((dr.h.al & 0xff) == 0x80) + { + xms_present = 1; + dr.x.ax = 0x4310; + __dpmi_int(0x2f, &dr); + __dpmi_raddr xms_entry; + xms_entry.segment = dr.x.es; + xms_entry.offset16 = dr.x.bx; + + __dpmi_regs xr = {}; + xr.h.ah = 0x08; /* Query Free Extended Memory */ + xr.x.cs = xms_entry.segment; + xr.x.ip = xms_entry.offset16; + __dpmi_simulate_real_mode_procedure_retf(&xr); + + if (xr.h.bl == 0) + xms_free_kb = xr.x.dx; + } + + uint32_t xms_total_kb = query_ext_mem_total_kb(); + + if (!xms_present && xms_total_kb > 0) + { + /* No XMS manager loaded, so nothing has claimed any of it yet. */ + xms_free_kb = xms_total_kb; + xms_present = 1; + } + + if (xms_total_kb < xms_free_kb) + { + /* + * The BIOS-reported extended memory size is a legacy/compatibility + * figure and can be smaller than the pool the XMS manager actually + * hands out (observed under dosemu2, whose virtual XMS pool isn't + * tied to what INT 15h reports). Never show a total smaller than + * what XMS itself claims is free - that would be a nonsensical + * "free > total" display. + */ + xms_total_kb = xms_free_kb; + } + + uint32_t xms_used_kb = xms_total_kb - xms_free_kb; + + /* Query EMS */ + uint32_t ems_total_kb = 0; + uint32_t ems_free_kb = 0; + int ems_present = 0; + + __dpmi_raddr int67; + __dpmi_get_real_mode_interrupt_vector(0x67, &int67); + if (int67.segment != 0) + { + __dpmi_regs er = {}; + er.h.ah = 0x40; + __dpmi_int(0x67, &er); + if (er.h.ah == 0) + { + er.h.ah = 0x42; + __dpmi_int(0x67, &er); + if (er.h.ah == 0) + { + ems_present = 1; + ems_free_kb = (uint32_t)er.x.bx * EMS_PAGE_KB; + ems_total_kb = (uint32_t)er.x.dx * EMS_PAGE_KB; + } + } + } + + /* Print Memory Summary Table */ + char str_tot[24], str_used[24], str_free[24]; + + print_line(""); + print_line("Memory Type Total Used Free"); + print_line("---------------- -------- -------- --------"); + + format_kb(conv_total_kb, str_tot, sizeof(str_tot)); + format_kb(conv_used_kb, str_used, sizeof(str_used)); + format_kb(conv_free_kb, str_free, sizeof(str_free)); + snprintf(line_buf, sizeof(line_buf), "%-16s %8s %8s %8s", + "Conventional", str_tot, str_used, str_free); + print_line(line_buf); + + format_kb(umb_total_kb, str_tot, sizeof(str_tot)); + format_kb(umb_used_kb, str_used, sizeof(str_used)); + format_kb(umb_free_kb, str_free, sizeof(str_free)); + snprintf(line_buf, sizeof(line_buf), "%-16s %8s %8s %8s", + "Upper", str_tot, str_used, str_free); + print_line(line_buf); + + format_kb(RESERVED_TOTAL_KB, str_tot, sizeof(str_tot)); + format_kb(0, str_free, sizeof(str_free)); + snprintf(line_buf, sizeof(line_buf), "%-16s %8s %8s %8s", + "Reserved", str_tot, str_tot, str_free); + print_line(line_buf); + + if (xms_present) + { + format_kb(xms_total_kb, str_tot, sizeof(str_tot)); + format_kb(xms_used_kb, str_used, sizeof(str_used)); + format_kb(xms_free_kb, str_free, sizeof(str_free)); + snprintf(line_buf, sizeof(line_buf), "%-16s %8s %8s %8s", + "Extended (XMS)", str_tot, str_used, str_free); + print_line(line_buf); + } + + print_line("---------------- -------- -------- --------"); + + format_kb(conv_total_kb + umb_total_kb + RESERVED_TOTAL_KB + xms_total_kb, + str_tot, sizeof(str_tot)); + format_kb(conv_used_kb + umb_used_kb + RESERVED_TOTAL_KB + xms_used_kb, + str_used, sizeof(str_used)); + format_kb(conv_free_kb + umb_free_kb + xms_free_kb, str_free, sizeof(str_free)); + snprintf(line_buf, sizeof(line_buf), "%-16s %8s %8s %8s", + "Total memory", str_tot, str_used, str_free); + print_line(line_buf); + + print_line(""); + + /* the reserved region is left out here, as MEM.EXE leaves it out */ + format_kb(conv_total_kb + umb_total_kb, str_tot, sizeof(str_tot)); + format_kb(conv_used_kb + umb_used_kb, str_used, sizeof(str_used)); + format_kb(conv_free_kb + umb_free_kb, str_free, sizeof(str_free)); + snprintf(line_buf, sizeof(line_buf), "%-16s %8s %8s %8s", + "Total under 1 MB", str_tot, str_used, str_free); + print_line(line_buf); + + print_line(""); + + if (ems_present) + { + print_kb_and_bytes("Total Expanded (EMS)", ems_total_kb, ems_total_kb * 1024); + print_kb_and_bytes("Free Expanded (EMS)", ems_free_kb, ems_free_kb * 1024); + print_line(""); + } + + print_kb_and_bytes("Largest executable program size", + paras_to_kb(conv_largest_paras), conv_largest_paras * 16); + print_kb_and_bytes("Largest available upper memory block", + paras_to_kb(umb_largest_paras), umb_largest_paras * 16); +} diff --git a/src/mem.h b/src/mem.h new file mode 100644 index 0000000..eae72cd --- /dev/null +++ b/src/mem.h @@ -0,0 +1,25 @@ +/* + * comcom64 - 64bit command.com + * mem.h: FreeDOS-like MEM command header + * Copyright (C) 2026 @stsp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MEM_H +#define MEM_H + +void perform_mem(const char *arg); + +#endif diff --git a/tests/compare_mem.sh b/tests/compare_mem.sh new file mode 100755 index 0000000..6542acf --- /dev/null +++ b/tests/compare_mem.sh @@ -0,0 +1,212 @@ +#!/bin/sh +# compare_mem.sh: run comcom64's built-in MEM against the real FreeDOS +# MEM.EXE, in the same dosemu2 session, and print both outputs so the +# numbers can be compared by hand (or diffed by a future caller). +# +# Both dosemu2 and FreeDOS come from the dosemu2 PPA: install dosemu2 +# and install-freedos from ppa:dosemu2/ppa (see ci_test_prereq.sh), and +# this script then runs dosemu2's own "insfdusr" to install the FreeDOS +# userspace - which is where MEM.EXE comes from - instead of downloading +# and unpacking mem.zip by hand. +# +# The FreeDOS installation lives in a private HOME under $CACHE_DIR, so +# it is done once and reused, and the caller's own ~/.dosemu is left +# alone. +# +# The same comparison is done by dosemu2's own test suite, in +# test/func_comcom_internal.py, which CI runs - this script is the +# stand-alone version of it, for looking at the two reports side by side. +# +# This does NOT try to byte-for-byte diff the two outputs: they format +# the detail listings differently and, since MEM.EXE is loaded as an external program +# while comcom64's MEM is built into the shell, the two tools observe +# slightly different conventional-memory states (MEM.EXE's own PSP, +# environment and program taking up space that our built-in command +# doesn't need). We run our own MEM first so it always sees the +# pristine, nothing-else-loaded state, then MEM.EXE second. +# +# What IS asserted automatically: the figures that are independent of +# which tool loaded them - conventional and upper totals, the reserved +# region, XMS total/used/free, EMS total/free, free upper memory and the +# largest free upper block. Conventional used/free differ by about a KB, +# which is MEM.EXE's own PSP and environment - our MEM is built into the +# shell and needs neither - so they are not asserted here. +# +# Usage: ./tests/compare_mem.sh [path/to/comcom64-build] +# The argument is the comcom64 to test: either a directory holding +# command.com (as "make install" produces), or a comcom64.exe file. +# It defaults to the build in src/, built via "make" if not there yet. +# Without an argument and without a build, the comcom64 that dosemu2 +# is configured with is used as-is. +# +# Env vars: +# CACHE_DIR - where to keep the FreeDOS installation used for the +# comparison (default: ~/.cache/comcom64-tests) +# EMS_SIZE_KB - EMS pool size to configure dosemu with (default: 4096) + +set -e + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd) + +CACHE_DIR=${CACHE_DIR:-"$HOME/.cache/comcom64-tests"} +EMS_SIZE_KB=${EMS_SIZE_KB:-4096} + +if ! command -v dosemu >/dev/null; then + echo "dosemu2 not found - install it from ppa:dosemu2/ppa:" >&2 + echo " sudo add-apt-repository ppa:dosemu2/ppa" >&2 + echo " sudo apt install dosemu2 install-freedos" >&2 + exit 1 +fi + +# The comcom64 under test. dosemu2 picks the shell up from +# $DOSEMU2_COMCOM_DIR, so a build can be tested without installing it. +COMCOM=${1:-} +if [ -z "$COMCOM" ] && [ ! -f "$REPO_ROOT/src/comcom64.exe" ] && + [ ! -f "$REPO_ROOT/src/libtmp.so" ]; then + echo "== no build in src/, building it ==" + make -C "$REPO_ROOT/src" -j"$(nproc)" || make -C "$REPO_ROOT" static +fi +if [ -z "$COMCOM" ]; then + if [ -f "$REPO_ROOT/src/comcom64.exe" ]; then + COMCOM="$REPO_ROOT/src/comcom64.exe" + else + COMCOM="$REPO_ROOT/src/libtmp.so" + fi +fi + +WORKDIR=$(mktemp -d) +trap 'rm -rf "$WORKDIR"' EXIT + +COMCOM_DIR="$WORKDIR/comcom" +mkdir -p "$COMCOM_DIR" +if [ -d "$COMCOM" ]; then + cp -a "$COMCOM"/. "$COMCOM_DIR/" +elif [ "${COMCOM%.so}" != "$COMCOM" ]; then + # dj64 static build: the loader runs the ELF next to it + ELFLOAD=$(pkg-config --variable=elfload dj64) + cp "$COMCOM" "$COMCOM_DIR/command.elf" + cp "$ELFLOAD" "$COMCOM_DIR/command.com" +else + cp "$COMCOM" "$COMCOM_DIR/command.com" +fi +echo "== testing comcom64 from $COMCOM ==" +export DOSEMU2_COMCOM_DIR="$COMCOM_DIR" + +# Private HOME: insfdusr installs FreeDOS into $HOME/.dosemu/drive_c. +TEST_HOME="$CACHE_DIR/home" +mkdir -p "$TEST_HOME" +export HOME="$TEST_HOME" + +if [ ! -f "$TEST_HOME/.dosemu/drive_c/bin/mem.exe" ]; then + echo "== installing the FreeDOS userspace with insfdusr ==" + timeout 600 dosemu -td -o "$WORKDIR/insfdusr.log" -E insfdusr \ + > "$WORKDIR/insfdusr.out" 2>&1 || { + echo "insfdusr failed:" >&2 + cat "$WORKDIR/insfdusr.out" >&2 + exit 1 + } + if [ ! -f "$TEST_HOME/.dosemu/drive_c/bin/mem.exe" ]; then + echo "insfdusr did not install MEM.EXE - is install-freedos installed?" >&2 + exit 1 + fi +fi + +cat > "$TEST_HOME/.dosemu/drive_c/cmpmem.bat" <<'EOF' +@echo off +echo ===COMCOM64-DEFAULT=== +mem +echo ===COMCOM64-DEBUG=== +mem /debug +echo ===COMCOM64-FREE=== +mem /free +echo ===COMCOM64-CLASSIFY=== +mem /classify +echo ===FDMEM-DEFAULT=== +c:\bin\mem.exe +echo ===FDMEM-FREE=== +c:\bin\mem.exe /free +echo ===FDMEM-CLASSIFY=== +c:\bin\mem.exe /classify +EOF + +OUT="$WORKDIR/output.txt" +timeout 120 dosemu -td \ + -e "$EMS_SIZE_KB" \ + -o "$WORKDIR/boot.log" \ + -E "cmpmem.bat" > "$OUT" 2>&1 || { + echo "dosemu run failed; boot.log follows:" >&2 + cat "$WORKDIR/boot.log" >&2 + exit 1 + } + +echo +echo "############################################################" +echo "# Full output (comcom64 first, then real FreeDOS MEM.EXE)" +echo "############################################################" +sed -n '/===COMCOM64-DEFAULT===/,$p' "$OUT" +echo + +sed -n '/===COMCOM64-DEFAULT===/,/===COMCOM64-DEBUG===/p' "$OUT" > "$WORKDIR/comcom.txt" +sed -n '/===FDMEM-DEFAULT===/,/===FDMEM-FREE===/p' "$OUT" > "$WORKDIR/fdmem.txt" + +# Both tools report the summary in KB now, so the figures can be compared +# directly. FreeDOS MEM prints "637K", comcom64 prints the same - strip +# the separators and the K suffix and compare the numbers. +field() { + # $1 = file, $2 = row regex, $3 = column (1=total, 2=used, 3=free) + grep -E "$2" "$1" | head -1 | sed -E 's/^.{16}//' | tr -d '\r' | + awk -v col="$3" '{print $col}' | tr -d ',K' +} + +status=0 +echo "############################################################" +echo "# Automated checks (values that must match exactly)" +echo "############################################################" + +check() { + name=$1; a=$2; b=$3 + if [ "$a" = "$b" ] && [ -n "$a" ]; then + echo "PASS: $name matches (${a}K)" + else + echo "FAIL: $name differs: comcom64=${a}K freedos=${b}K" + status=1 + fi +} + +check_row() { + # $1 = label, $2 = row regex, $3 = column + check "$1" "$(field "$WORKDIR/comcom.txt" "$2" "$3")" \ + "$(field "$WORKDIR/fdmem.txt" "$2" "$3")" +} + +check_row "conventional total" '^Conventional' 1 +check_row "upper total" '^Upper' 1 +check_row "upper used" '^Upper' 2 +check_row "upper free" '^Upper' 3 +check_row "reserved total" '^Reserved' 1 +check_row "reserved used" '^Reserved' 2 +check_row "XMS total" '^Extended \(XMS\)' 1 +check_row "XMS used" '^Extended \(XMS\)' 2 +check_row "XMS free" '^Extended \(XMS\)' 3 +check_row "total under 1 MB" '^Total under 1 MB' 1 + +tail_field() { + # $1 = file, $2 = label regex: the KB figure of a "label NNNK" line. + # FreeDOS MEM appends the byte count in brackets, so pick the first + # number that carries the K suffix rather than the last field. + grep -E "$2" "$1" | head -1 | tr -d '\r' | + grep -oE '[0-9][0-9,]*K' | head -1 | tr -d ',K' +} + +check "EMS total" "$(tail_field "$WORKDIR/comcom.txt" '^Total Expanded \(EMS\)')" \ + "$(tail_field "$WORKDIR/fdmem.txt" '^Total Expanded \(EMS\)')" +check "EMS free" "$(tail_field "$WORKDIR/comcom.txt" '^Free Expanded \(EMS\)')" \ + "$(tail_field "$WORKDIR/fdmem.txt" '^Free Expanded \(EMS\)')" +# MEM.EXE calls this row "Largest free upper memory block", ours keeps +# the MS-DOS wording that the dosemu2 test suite looks for. +check "largest upper memory block" \ + "$(tail_field "$WORKDIR/comcom.txt" '^Largest available upper')" \ + "$(tail_field "$WORKDIR/fdmem.txt" '^Largest free upper')" + +exit $status