diff --git a/ci_prereq.sh b/ci_prereq.sh
index 25f82b8..029445a 100755
--- a/ci_prereq.sh
+++ b/ci_prereq.sh
@@ -5,6 +5,20 @@ sudo apt install -y \
devscripts \
equivs
-sudo add-apt-repository ppa:stsp-0/thunk-gen
-sudo add-apt-repository ppa:stsp-0/dj64
+add_ppa() {
+ PPA="$1"
+ for i in 1 2 3 4 5 6 7 8 9 10; do
+ echo "Adding $PPA (attempt $i)..."
+ if sudo add-apt-repository -y "$PPA"; then
+ return 0
+ fi
+ sleep 8
+ done
+ return 1
+}
+
+add_ppa ppa:stsp-0/thunk-gen
+add_ppa ppa:stsp-0/dj64
+
+sudo apt update
mk-build-deps --install --root-cmd sudo
diff --git a/ci_test_prereq.sh b/ci_test_prereq.sh
index 66b0d54..913873b 100755
--- a/ci_test_prereq.sh
+++ b/ci_test_prereq.sh
@@ -1,6 +1,18 @@
#!/bin/sh
-sudo add-apt-repository ppa:dosemu2/ppa
+add_ppa() {
+ PPA="$1"
+ for i in 1 2 3 4 5 6 7 8 9 10; do
+ echo "Adding $PPA (attempt $i)..."
+ if sudo add-apt-repository -y "$PPA"; then
+ return 0
+ fi
+ sleep 8
+ done
+ return 1
+}
+
+add_ppa ppa:dosemu2/ppa
sudo apt update -q
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 49ec67f..1fd4683 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, "", "display memory statistics"},
{"move", perform_move, "", "move file"},
{"more", perform_more, "", "scroll-pause long output"},
{"mouseopt", perform_mouseopt, "", "mouse options"},
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..4828143
--- /dev/null
+++ b/src/mem.c
@@ -0,0 +1,612 @@
+/*
+ * comcom64 - 64bit command.com
+ * mem.c: FreeDOS-like MEM command implementation
+ * 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 .
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "asm.h"
+#include "command.h"
+#include "umb.h"
+#include "mem.h"
+
+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[9];
+ uint32_t conv_bytes;
+ uint32_t umb_bytes;
+};
+
+static int opt_classify = 0;
+static int opt_free = 0;
+static int opt_debug = 0;
+static int opt_page = 0;
+static int page_line_count = 0;
+
+static void reset_page(void)
+{
+ page_line_count = 0;
+}
+
+static void print_line(const char *str)
+{
+ printf("%s\n", str);
+ if (opt_page)
+ {
+ page_line_count++;
+ if (page_line_count >= 23)
+ {
+ printf("Press any key to continue . . .");
+ fflush(stdout);
+ getch();
+ printf("\r \r");
+ page_line_count = 0;
+ }
+ }
+}
+
+static void format_number(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;
+
+ for (int 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 get_mcb_name(uint16_t seg, const struct MCB *mcb, char *name_out, size_t name_out_size)
+{
+ memset(name_out, 0, name_out_size);
+
+ if (mcb->owner_psp == 0)
+ {
+ strncpy(name_out, "FREE", name_out_size - 1);
+ return;
+ }
+ if (mcb->owner_psp == 8)
+ {
+ strncpy(name_out, "SYSTEM", name_out_size - 1);
+ return;
+ }
+
+ /* Check if mcb->name contains printable characters */
+ int valid_mcb_name = 1;
+ for (int i = 0; i < 8; i++)
+ {
+ if (mcb->name[i] == 0)
+ break;
+ if (!isprint((unsigned char)mcb->name[i]))
+ {
+ valid_mcb_name = 0;
+ break;
+ }
+ }
+
+ if (valid_mcb_name && mcb->name[0] != 0 && mcb->name[0] != ' ')
+ {
+ int i = 0;
+ for (; i < 8 && mcb->name[i] != 0 && !isspace((unsigned char)mcb->name[i]); i++)
+ {
+ name_out[i] = mcb->name[i];
+ }
+ name_out[i] = '\0';
+ if (strlen(name_out) > 0)
+ 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);
+}
+
+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)
+{
+ opt_classify = 0;
+ opt_free = 0;
+ opt_debug = 0;
+ opt_page = 0;
+
+ const char *p = arg;
+ while (*p)
+ {
+ while (isspace((unsigned char)*p))
+ p++;
+ if (*p == '/' || *p == '-')
+ {
+ p++;
+ if (strnicmp(p, "CLASSIFY", 8) == 0 || strnicmp(p, "C", 1) == 0)
+ {
+ opt_classify = 1;
+ while (isalnum((unsigned char)*p))
+ p++;
+ }
+ else if (strnicmp(p, "FREE", 4) == 0 || strnicmp(p, "F", 1) == 0)
+ {
+ opt_free = 1;
+ while (isalnum((unsigned char)*p))
+ p++;
+ }
+ else if (strnicmp(p, "DEBUG", 5) == 0 || strnicmp(p, "D", 1) == 0)
+ {
+ opt_debug = 1;
+ while (isalnum((unsigned char)*p))
+ p++;
+ }
+ else if (strnicmp(p, "PAGE", 4) == 0 || strnicmp(p, "P", 1) == 0)
+ {
+ opt_page = 1;
+ while (isalnum((unsigned char)*p))
+ p++;
+ }
+ else if (*p == '?' || strnicmp(p, "HELP", 4) == 0)
+ {
+ show_help();
+ return;
+ }
+ else
+ {
+ p++;
+ }
+ }
+ else
+ {
+ p++;
+ }
+ }
+
+ reset_page();
+
+ /* Save UMB link state and link UMBs for memory scan */
+ __dpmi_regs r = {};
+ r.x.ax = 0x5800;
+ __dpmi_int(0x21, &r);
+ uint16_t orig_strat = r.x.ax;
+
+ r.x.ax = 0x5802;
+ __dpmi_int(0x21, &r);
+ uint8_t orig_umblink = r.h.al;
+
+ link_umb(orig_strat);
+
+ /* 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 = 0;
+ uint32_t conv_free = 0;
+ uint32_t conv_largest = 0;
+
+ uint32_t umb_total = 0;
+ uint32_t umb_free = 0;
+ uint32_t umb_largest = 0;
+
+ struct module_entry modules[64];
+ int num_modules = 0;
+ memset(modules, 0, sizeof(modules));
+
+ char line_buf[160];
+
+ if (opt_debug)
+ {
+ print_line("Memory Detail:");
+ print_line(" Segment Owner Size (Bytes) Type/Name");
+ print_line(" ------- ------- -------------- -----------");
+ }
+ else if (opt_free)
+ {
+ print_line("Free Memory Blocks:");
+ print_line(" Segment Size (Bytes) 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_bytes = (uint32_t)mcb.size * 16;
+ int is_umb = (curr_seg >= 0xA000);
+
+ char name[16];
+ get_mcb_name(curr_seg, &mcb, name, sizeof(name));
+
+ if (is_umb)
+ {
+ umb_total += block_bytes + 16; /* include MCB header */
+ if (mcb.owner_psp == 0)
+ {
+ umb_free += block_bytes;
+ if (block_bytes > umb_largest)
+ umb_largest = block_bytes;
+ }
+ }
+ else
+ {
+ conv_total += block_bytes + 16; /* include MCB header */
+ if (mcb.owner_psp == 0)
+ {
+ conv_free += block_bytes;
+ if (block_bytes > conv_largest)
+ conv_largest = block_bytes;
+ }
+ }
+
+ /* Collect module info for /CLASSIFY */
+ if (opt_classify)
+ {
+ int found = -1;
+ for (int 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_bytes += block_bytes + 16;
+ else
+ modules[found].conv_bytes += block_bytes + 16;
+ }
+ }
+
+ if (opt_debug)
+ {
+ char sz_str[32];
+ format_number(block_bytes, sz_str, sizeof(sz_str));
+ snprintf(line_buf, sizeof(line_buf), " %04Xh %04Xh %14s %s",
+ curr_seg, mcb.owner_psp, sz_str, name);
+ print_line(line_buf);
+ }
+ else if (opt_free && mcb.owner_psp == 0)
+ {
+ char sz_str[32];
+ format_number(block_bytes, sz_str, sizeof(sz_str));
+ snprintf(line_buf, sizeof(line_buf), " %04Xh %14s %s",
+ curr_seg, sz_str, is_umb ? "Upper" : "Conventional");
+ print_line(line_buf);
+ }
+
+ if (mcb.id == 'Z')
+ break;
+
+ curr_seg += 1 + mcb.size;
+ }
+
+ if (!orig_umblink)
+ unlink_umb();
+
+ /* Fill default conventional memory total if 0 */
+ if (conv_total == 0 || conv_total < 640 * 1024)
+ {
+ __dpmi_regs ir = {};
+ __dpmi_int(0x12, &ir);
+ if (ir.x.ax > 0)
+ conv_total = (uint32_t)ir.x.ax * 1024;
+ else
+ conv_total = 640 * 1024;
+ }
+
+ if (opt_classify)
+ {
+ print_line("");
+ print_line("Modules using memory below 1 MB:");
+ print_line("");
+ print_line(" Name Total Conventional Upper Memory");
+ print_line(" -------- ----------------- ---------------- ----------------");
+
+ for (int i = 0; i < num_modules; i++)
+ {
+ uint32_t tot = modules[i].conv_bytes + modules[i].umb_bytes;
+ char t_str[24], c_str[24], u_str[24];
+ format_number(tot, t_str, sizeof(t_str));
+ format_number(modules[i].conv_bytes, c_str, sizeof(c_str));
+ format_number(modules[i].umb_bytes, u_str, sizeof(u_str));
+
+ snprintf(line_buf, sizeof(line_buf), " %-8s %12s (%3luK) %12s (%3luK) %12s (%3luK)",
+ modules[i].name,
+ t_str, (unsigned long)(tot / 1024),
+ c_str, (unsigned long)(modules[i].conv_bytes / 1024),
+ u_str, (unsigned long)(modules[i].umb_bytes / 1024));
+ print_line(line_buf);
+ }
+ print_line("");
+ }
+
+ /* Query XMS */
+ uint32_t xms_free_kb = 0;
+ uint32_t xms_largest_free_kb = 0;
+ int xms_present = 0;
+
+ r.x.ax = 0x4300;
+ __dpmi_int(0x2f, &r);
+ if ((r.h.al & 0xff) == 0x80)
+ {
+ xms_present = 1;
+ r.x.ax = 0x4310;
+ __dpmi_int(0x2f, &r);
+ __dpmi_raddr xms_entry;
+ xms_entry.segment = r.x.es;
+ xms_entry.offset16 = r.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_largest_free_kb = xr.x.ax;
+ xms_free_kb = xr.x.dx;
+ }
+ }
+
+ if (!xms_present || xms_free_kb == 0)
+ {
+ __dpmi_regs xr = {};
+ xr.h.ah = 0x88;
+ __dpmi_int(0x15, &xr);
+ if (!(xr.x.flags & 1) && xr.x.ax > 0)
+ {
+ xms_free_kb = xr.x.ax;
+ xms_largest_free_kb = xr.x.ax;
+ xms_present = 1;
+ }
+ }
+
+ /* 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 * 16;
+ ems_total_kb = (uint32_t)er.x.dx * 16;
+ }
+ }
+ }
+
+ /* Print Memory Summary Table */
+ print_line("");
+ print_line("Memory Type Total Used Free");
+ print_line("---------------- ----------- ----------- -----------");
+
+ uint32_t conv_used = (conv_total > conv_free) ? (conv_total - conv_free) : 0;
+ uint32_t umb_used = (umb_total > umb_free) ? (umb_total - umb_free) : 0;
+ uint32_t xms_total_bytes = xms_free_kb * 1024;
+ uint32_t xms_free_bytes = xms_free_kb * 1024;
+ uint32_t xms_used_bytes = 0;
+
+ char str_c_tot[20], str_c_used[20], str_c_free[20];
+ format_number(conv_total, str_c_tot, sizeof(str_c_tot));
+ format_number(conv_used, str_c_used, sizeof(str_c_used));
+ format_number(conv_free, str_c_free, sizeof(str_c_free));
+
+ snprintf(line_buf, sizeof(line_buf), "Conventional %11s %11s %11s",
+ str_c_tot, str_c_used, str_c_free);
+ print_line(line_buf);
+
+ if (umb_total > 0)
+ {
+ char str_u_tot[20], str_u_used[20], str_u_free[20];
+ format_number(umb_total, str_u_tot, sizeof(str_u_tot));
+ format_number(umb_used, str_u_used, sizeof(str_u_used));
+ format_number(umb_free, str_u_free, sizeof(str_u_free));
+
+ snprintf(line_buf, sizeof(line_buf), "Upper %11s %11s %11s",
+ str_u_tot, str_u_used, str_u_free);
+ print_line(line_buf);
+ }
+
+ if (xms_present)
+ {
+ char str_x_tot[20], str_x_used[20], str_x_free[20];
+ format_number(xms_total_bytes, str_x_tot, sizeof(str_x_tot));
+ format_number(xms_used_bytes, str_x_used, sizeof(str_x_used));
+ format_number(xms_free_bytes, str_x_free, sizeof(str_x_free));
+
+ snprintf(line_buf, sizeof(line_buf), "Extended (XMS) %11s %11s %11s",
+ str_x_tot, str_x_used, str_x_free);
+ print_line(line_buf);
+ }
+
+ print_line("---------------- ----------- ----------- -----------");
+
+ uint32_t grand_tot = conv_total + umb_total + xms_total_bytes;
+ uint32_t grand_used = conv_used + umb_used + xms_used_bytes;
+ uint32_t grand_free = conv_free + umb_free + xms_free_bytes;
+
+ char str_g_tot[20], str_g_used[20], str_g_free[20];
+ format_number(grand_tot, str_g_tot, sizeof(str_g_tot));
+ format_number(grand_used, str_g_used, sizeof(str_g_used));
+ format_number(grand_free, str_g_free, sizeof(str_g_free));
+
+ snprintf(line_buf, sizeof(line_buf), "Total memory %11s %11s %11s",
+ str_g_tot, str_g_used, str_g_free);
+ print_line(line_buf);
+
+ print_line("");
+
+ uint32_t u1mb_tot = conv_total + umb_total;
+ uint32_t u1mb_used = conv_used + umb_used;
+ uint32_t u1mb_free = conv_free + umb_free;
+
+ char str_1_tot[20], str_1_used[20], str_1_free[20];
+ format_number(u1mb_tot, str_1_tot, sizeof(str_1_tot));
+ format_number(u1mb_used, str_1_used, sizeof(str_1_used));
+ format_number(u1mb_free, str_1_free, sizeof(str_1_free));
+
+ snprintf(line_buf, sizeof(line_buf), "Total under 1 MB %11s %11s %11s",
+ str_1_tot, str_1_used, str_1_free);
+ print_line(line_buf);
+
+ print_line("");
+
+ if (ems_present)
+ {
+ char str_ems_tot[20], str_ems_free[20];
+ format_number(ems_total_kb * 1024, str_ems_tot, sizeof(str_ems_tot));
+ format_number(ems_free_kb * 1024, str_ems_free, sizeof(str_ems_free));
+
+ snprintf(line_buf, sizeof(line_buf), "Total Expanded (EMS) %10lu KB (%s bytes)",
+ (unsigned long)ems_total_kb, str_ems_tot);
+ print_line(line_buf);
+
+ snprintf(line_buf, sizeof(line_buf), "Free Expanded (EMS) %10lu KB (%s bytes)",
+ (unsigned long)ems_free_kb, str_ems_free);
+ print_line(line_buf);
+ print_line("");
+ }
+
+ char str_c_lrg[20];
+ format_number(conv_largest, str_c_lrg, sizeof(str_c_lrg));
+ snprintf(line_buf, sizeof(line_buf), "Largest executable program size %7lu KB (%s bytes)",
+ (unsigned long)(conv_largest / 1024), str_c_lrg);
+ print_line(line_buf);
+
+ if (umb_total > 0)
+ {
+ char str_u_lrg[20];
+ format_number(umb_largest, str_u_lrg, sizeof(str_u_lrg));
+ snprintf(line_buf, sizeof(line_buf), "Largest available upper memory block %7lu KB (%s bytes)",
+ (unsigned long)(umb_largest / 1024), str_u_lrg);
+ print_line(line_buf);
+ }
+
+ print_line("MS-DOS is resident in the high memory area.");
+}
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