File size: 65,565 Bytes
de4c45a
f79687c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d4df7cc
f79687c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d4df7cc
 
f79687c
d4df7cc
f79687c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
de4c45a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Linker Map Auditor - what is eating your FLASH and RAM</title>
<meta name="description" content="Paste a GNU ld .map file and see, in exact bytes, which object file and which archive is spending your FLASH and RAM - and by how much you overflowed.">
<style>
  :root{--bg:#0e1116;--panel:#161b22;--line:#272e38;--fg:#e6edf3;--dim:#8b949e;
        --ok:#3fb950;--bad:#f85149;--warn:#d29922;--acc:#58a6ff}
  *{box-sizing:border-box}
  body{margin:0;background:var(--bg);color:var(--fg);
       font:15px/1.5 ui-sans-serif,system-ui,"Segoe UI",Roboto,sans-serif}
  .wrap{max-width:1080px;margin:0 auto;padding:28px 20px 64px}
  h1{font-size:26px;margin:0 0 6px}
  .sub{color:var(--dim);margin:0 0 22px}
  .panel{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:18px;margin-bottom:16px}
  .verdict{font-size:22px;font-weight:700;margin:0 0 14px}
  .verdict.bad{color:var(--bad)} .verdict.ok{color:var(--ok)}
  table{width:100%;border-collapse:collapse;font:13px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
  th,td{padding:6px 8px;border-bottom:1px solid var(--line);text-align:right;white-space:nowrap}
  th:first-child,td:first-child{text-align:left}
  th{color:var(--dim);font-weight:600;font-size:11px;letter-spacing:.04em;text-transform:uppercase}
  td.name{white-space:normal;word-break:break-all}
  .bar{height:7px;background:#20262e;border-radius:4px;overflow:hidden;margin-top:5px;min-width:120px}
  .bar>i{display:block;height:100%;background:var(--ok)}
  .bar>i.bad{background:var(--bad)} .bar>i.warn{background:var(--warn)}
  .grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
  @media(max-width:800px){.grid{grid-template-columns:1fr}}
  h3{font-size:13px;color:var(--dim);text-transform:uppercase;letter-spacing:.04em;margin:0 0 8px}
  label{display:block;font-size:12px;color:var(--dim);margin:0 0 5px}
  select,textarea,input{width:100%;background:#0d1117;color:var(--fg);border:1px solid var(--line);
        border-radius:7px;padding:9px 10px;font:13px ui-monospace,Menlo,Consolas,monospace}
  textarea{min-height:132px;resize:vertical}
  button{background:var(--acc);color:#06121f;border:0;border-radius:7px;padding:10px 18px;
         font-size:14px;font-weight:700;cursor:pointer}
  .row{display:flex;gap:12px;align-items:flex-end;margin-top:12px;flex-wrap:wrap}
  .row>div{flex:1;min-width:180px}
  .buy{border:1px solid #2b4a6f;background:#111a24}
  .buy ul{margin:8px 0 12px;padding-left:18px;color:var(--fg)}
  .buy li{margin:3px 0;font-size:14px}
  .buy a.cta{display:inline-block;background:var(--acc);color:#06121f;text-decoration:none;
             font-weight:700;padding:10px 18px;border-radius:7px}
  .terms{color:var(--dim);font-size:12px;margin-top:10px}
  .note{color:var(--dim);font-size:12px;margin-top:10px}
  code{background:#0d1117;border:1px solid var(--line);border-radius:4px;padding:1px 5px;font-size:12px}
  footer{color:var(--dim);font-size:12px;margin-top:28px;border-top:1px solid var(--line);padding-top:14px}
</style>
<script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
<!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/linker-map-auditor"></head>
<body>
<div class="wrap">
  <h1>Linker Map Auditor</h1>
  <p class="sub">Paste the <code>.map</code> your linker already wrote. Get the exact bytes: which region
     overflowed, and which object file spent them. Runs entirely in this tab - the file never leaves your machine.</p>

  <div class="panel" id="result"></div>

  <div class="panel buy">
    <h3>Free here, for one build</h3>
    <p style="margin:0 0 4px">Everything above is free, for every map file you have, forever. The full version
       answers the question one build cannot: <strong>what changed since last time.</strong></p>
    <ul>
      <li>Save a known-good build as a <strong>baseline</strong> and diff any later build against it</li>
      <li>See the exact object file that grew, and by how many bytes</li>
      <li>Per-region <strong>budgets</strong> (<code>{"FLASH":"90%","RAM":"7K"}</code>) that pass or fail</li>
      <li>Export <code>linker-map-report.json</code> + <code>.md</code> for CI and for your team</li>
    </ul>
    <a class="cta" href="https://getreadystack.com/api/buy/cl/polar_cl_2x41azB4vGEeUd5HdFkPGOiAorn6HYGk0WDro17qEb0">Get the full version - $29</a>
    <p class="terms">$29 once - one licence key per person or team seat.
       A freelance embedded engineer averages $103/hr (contractrates.fyi, 2026); this is about 17 minutes of one.</p>
  </div>

  <div class="panel">
    <h3>Your map file</h3>
    <div class="row">
      <div>
        <label for="example">Example build</label>
        <select id="example"><option value="over">Build A - 16K/8K MCU, RAM overflows</option><option value="base">Build B - the same firmware one week earlier</option><option value="fixed">Build C - after shrinking the UART and log buffers</option></select>
      </div>
      <div style="max-width:150px">
        <label for="topn">Top spenders</label>
        <input id="topn" type="number" min="1" max="60" value="12">
      </div>
      <div style="flex:0 0 auto"><button id="run">Analyze this map</button></div>
    </div>
    <div style="margin-top:12px">
      <label for="maptext">Or paste your own map file (generated with <code>-Wl,-Map=build/app.map</code>)</label>
      <textarea id="maptext" spellcheck="false"></textarea>
    </div>
    <p class="note">Works with any GNU ld map: arm-none-eabi, riscv, avr, msp430, and the map files written by
       STM32CubeIDE, MCUXpresso and PlatformIO.</p>
  </div>

  <footer>Linker Map Auditor - the same parser that runs in the VS Code extension runs on this page.</footer>
</div>

<script>'use strict';
/*
 * mapparse.js - the one brain.
 * Parses a GNU ld map file (arm-none-eabi-ld, riscv64-unknown-elf-ld, avr-ld, ...)
 * produced by `-Wl,-Map=out.map`, and answers, in exact bytes:
 *   - how much of each MEMORY region is used, free, or overflowed
 *   - which object files / archive members / input sections are spending it
 *   - what changed since a saved baseline
 *
 * The same file is loaded by extension.js (CommonJS) and inlined into
 * index.html (browser global MapParse). One brain, two doors.
 */
(function (root, factory) {
  if (typeof module === 'object' && module.exports) module.exports = factory();
  else root.MapParse = factory();
}(typeof self !== 'undefined' ? self : this, function () {

  var VERSION = '0.1.0';

  // -- helpers ---------------------------------------------------------
  function hex(s) { return parseInt(s, 16); }

  function commas(n) {
    var neg = n < 0; n = Math.abs(Math.round(n));
    var s = String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
    return (neg ? '-' : '') + s;
  }

  function humanBytes(n) {
    var a = Math.abs(n);
    if (a >= 1048576) return (n / 1048576).toFixed(2) + ' MiB';
    if (a >= 1024) return (n / 1024).toFixed(2) + ' KiB';
    return commas(n) + ' B';
  }

  // "512K" | "0x8000" | "90%" | 8192  ->  { bytes, text }
  function parseBudget(v, regionLength) {
    if (v === null || v === undefined || v === '') return null;
    if (typeof v === 'number') return { bytes: Math.round(v), text: commas(v) + ' B' };
    var s = String(v).trim();
    var m = s.match(/^(\d+(?:\.\d+)?)\s*%$/);
    if (m) {
      if (!regionLength) return null;
      return { bytes: Math.floor(regionLength * parseFloat(m[1]) / 100), text: m[1] + '%' };
    }
    m = s.match(/^0x([0-9a-fA-F]+)$/);
    if (m) return { bytes: hex(m[1]), text: s };
    m = s.match(/^(\d+(?:\.\d+)?)\s*([KMkm])?(?:i?[Bb])?$/);
    if (m) {
      var n = parseFloat(m[1]);
      var unit = (m[2] || '').toUpperCase();
      if (unit === 'K') n *= 1024;
      else if (unit === 'M') n *= 1048576;
      return { bytes: Math.round(n), text: s };
    }
    return null;
  }

  // "Drivers/libhal.a(stm32f4xx_hal.o)" -> archive + member
  function splitObject(spec) {
    var m = spec.match(/^(.*?)([^\/\\]+\.a)\(([^)]+)\)\s*$/);
    if (m) return { path: spec, archive: m[2], member: m[3], label: m[2] + '(' + m[3] + ')' };
    var base = spec.replace(/^.*[\/\\]/, '');
    return { path: spec, archive: null, member: base, label: base };
  }

  // -- the parser ------------------------------------------------------
  var RE_REGION = /^(\S+)\s+0x([0-9a-fA-F]+)\s+0x([0-9a-fA-F]+)(?:\s+(\S+))?\s*$/;
  var RE_OUT_FULL = /^([.$\w][^\s]*)\s+0x([0-9a-fA-F]+)\s+0x([0-9a-fA-F]+)(?:\s+load address\s+0x([0-9a-fA-F]+))?\s*$/;
  var RE_OUT_NAME = /^([.$\w][^\s]*)\s*$/;
  var RE_OUT_TAIL = /^\s+0x([0-9a-fA-F]+)\s+0x([0-9a-fA-F]+)(?:\s+load address\s+0x([0-9a-fA-F]+))?\s*$/;
  var RE_IN_FULL = /^\s+(\S+)\s+0x([0-9a-fA-F]+)\s+0x([0-9a-fA-F]+)\s+(\S.*?)\s*$/;
  var RE_IN_FILL = /^\s+(\*fill\*)\s+0x([0-9a-fA-F]+)\s+0x([0-9a-fA-F]+)\s*$/;
  var RE_IN_NAME = /^\s+(\.[^\s]+|COMMON|\*fill\*)\s*$/;
  var RE_IN_TAIL = /^\s+0x([0-9a-fA-F]+)\s+0x([0-9a-fA-F]+)\s+(\S.*?)\s*$/;

  var FILL_LABEL = '(alignment fill)';
  var NOATTR_LABEL = '(not attributed to an input section)';

  function parseMap(text, name) {
    var lines = String(text == null ? '' : text).replace(/\r\n?/g, '\n').split('\n');
    var regions = [], sections = [], inputs = [], warnings = [];

    // 1) Memory Configuration
    var i = 0, inMem = false;
    for (; i < lines.length; i++) {
      var ln = lines[i];
      if (/^Memory Configuration\s*$/.test(ln)) { inMem = true; continue; }
      if (!inMem) continue;
      if (/^Linker script and memory map\s*$/.test(ln)) break;
      if (/^Name\s+Origin\s+Length/.test(ln)) continue;
      var mr = ln.match(RE_REGION);
      if (!mr || mr[1] === '*default*') continue;
      var origin = hex(mr[2]), length = hex(mr[3]);
      if (!isFinite(origin) || !isFinite(length) || length <= 0) continue;
      regions.push({
        name: mr[1], origin: origin, length: length,
        attrs: mr[4] || '', used: 0, unattributed: 0
      });
    }
    if (!regions.length) {
      warnings.push('No "Memory Configuration" block found - is this a GNU ld map file? ' +
        'Region totals cannot be computed without it.');
    }

    function regionOf(addr) {
      for (var k = 0; k < regions.length; k++) {
        var r = regions[k];
        if (addr >= r.origin && addr < r.origin + r.length) return r.name;
      }
      return null;
    }

    // 2) the map body
    var cur = null, pendingOut = null, pendingIn = null;

    function openSection(nm, addr, size, load) {
      var regs = [], rv = regionOf(addr);
      if (rv) regs.push(rv);
      if (load !== null && load !== undefined) {
        var rl = regionOf(load);
        if (rl && regs.indexOf(rl) === -1) regs.push(rl);
      }
      cur = { name: nm, addr: addr, size: size, load: (load === undefined ? null : load),
              regions: regs, attributed: 0 };
      sections.push(cur);
      // A section's own size is what the region actually spends (it includes
      // padding no input section claims), so region totals come from here.
      for (var k = 0; k < regs.length; k++) {
        for (var j = 0; j < regions.length; j++) {
          if (regions[j].name === regs[k]) regions[j].used += size;
        }
      }
    }

    function addInput(nm, addr, size, spec) {
      if (!cur || !size) return;
      var obj = spec === null
        ? { path: FILL_LABEL, archive: null, member: FILL_LABEL, label: FILL_LABEL }
        : splitObject(spec);
      cur.attributed += size;
      inputs.push({ section: nm, addr: addr, size: size, object: obj.label, path: obj.path,
                    archive: obj.archive, regions: cur.regions.slice(), parent: cur.name });
    }

    for (; i < lines.length; i++) {
      var line = lines[i];
      if (!line) { pendingOut = null; pendingIn = null; continue; }

      if (/^\s/.test(line)) {
        // indented: input section, or the tail of a wrapped name
        var mf = line.match(RE_IN_FILL);
        if (mf) { addInput('*fill*', hex(mf[2]), hex(mf[3]), null); pendingIn = null; pendingOut = null; continue; }

        if (pendingOut !== null) {
          var mot = line.match(RE_OUT_TAIL);
          if (mot) {
            openSection(pendingOut, hex(mot[1]), hex(mot[2]), mot[3] ? hex(mot[3]) : null);
            pendingOut = null; pendingIn = null; continue;
          }
          pendingOut = null;
        }
        if (pendingIn !== null) {
          var mit = line.match(RE_IN_TAIL);
          if (mit) { addInput(pendingIn, hex(mit[1]), hex(mit[2]), mit[3]); pendingIn = null; continue; }
          pendingIn = null;
        }
        var mi = line.match(RE_IN_FULL);
        if (mi && !/^0x/.test(mi[1])) { addInput(mi[1], hex(mi[2]), hex(mi[3]), mi[4]); continue; }
        var mn = line.match(RE_IN_NAME);
        if (mn) { pendingIn = mn[1]; continue; }
        continue;
      }

      // column 0: an output section (or linker noise we ignore)
      pendingIn = null;
      var mo = line.match(RE_OUT_FULL);
      if (mo) {
        openSection(mo[1], hex(mo[2]), hex(mo[3]), mo[4] ? hex(mo[4]) : null);
        pendingOut = null;
        continue;
      }
      var mnm = line.match(RE_OUT_NAME);
      if (mnm && /^[.$]/.test(mnm[1])) { pendingOut = mnm[1]; continue; }
      pendingOut = null;
    }

    // 3) roll up
    var byObject = {}, byArchive = {}, bySection = {};
    function bump(store, key, region, size, extra) {
      var e = store[key];
      if (!e) {
        e = store[key] = { name: key, total: 0, byRegion: {}, sections: {}, sectionsByRegion: {} };
        if (extra) for (var k in extra) if (Object.prototype.hasOwnProperty.call(extra, k)) e[k] = extra[k];
      }
      e.total += size;
      e.byRegion[region] = (e.byRegion[region] || 0) + size;
      return e;
    }
    for (var n = 0; n < inputs.length; n++) {
      var it = inputs[n];
      var regs = it.regions.length ? it.regions : ['(outside any region)'];
      for (var q = 0; q < regs.length; q++) {
        var rg = regs[q];
        var eo = bump(byObject, it.object, rg, it.size, { archive: it.archive, path: it.path });
        eo.sections[it.section] = (eo.sections[it.section] || 0) + it.size;
        // Per region as well: listing an object's FLASH sections inside the RAM
        // table told the customer to go shrink a section that costs no RAM.
        if (!eo.sectionsByRegion[rg]) eo.sectionsByRegion[rg] = {};
        eo.sectionsByRegion[rg][it.section] = (eo.sectionsByRegion[rg][it.section] || 0) + it.size;
        if (it.archive) bump(byArchive, it.archive, rg, it.size);
        bump(bySection, it.section + '  ' + it.object, rg, it.size,
             { section: it.section, object: it.object });
      }
    }

    // what no input section claimed, per region (padding, heap/stack reservations)
    for (var s = 0; s < sections.length; s++) {
      var sec = sections[s];
      var gap = sec.size - sec.attributed;
      if (gap <= 0) continue;
      for (var g = 0; g < sec.regions.length; g++) {
        for (var h = 0; h < regions.length; h++) {
          if (regions[h].name === sec.regions[g]) regions[h].unattributed += gap;
        }
      }
    }

    for (var r2 = 0; r2 < regions.length; r2++) {
      var R = regions[r2];
      R.free = R.length - R.used;
      R.pct = R.length ? (R.used * 100 / R.length) : 0;
      R.over = R.used > R.length ? (R.used - R.length) : 0;
    }

    function sortDesc(store) {
      var arr = [];
      for (var k in store) if (Object.prototype.hasOwnProperty.call(store, k)) arr.push(store[k]);
      arr.sort(function (a, b) { return b.total - a.total; });
      return arr;
    }

    return {
      version: VERSION,
      name: name || '',
      regions: regions,
      sections: sections,
      inputs: inputs,
      objects: sortDesc(byObject),
      archives: sortDesc(byArchive),
      inputSections: sortDesc(bySection),
      warnings: warnings,
      stats: {
        lines: lines.length, sections: sections.length, inputs: inputs.length,
        objects: Object.keys(byObject).length, archives: Object.keys(byArchive).length
      }
    };
  }

  // top spenders inside one region
  function topIn(parsed, regionName, limit) {
    var out = [];
    for (var i = 0; i < parsed.objects.length; i++) {
      var o = parsed.objects[i], v = o.byRegion[regionName];
      if (v) {
        var secs = (o.sectionsByRegion && o.sectionsByRegion[regionName]) || o.sections;
        out.push({ name: o.name, size: v, sections: secs, archive: o.archive });
      }
    }
    out.sort(function (a, b) { return b.size - a.size; });
    return limit ? out.slice(0, limit) : out;
  }

  function regionByName(parsed, nm) {
    for (var i = 0; i < parsed.regions.length; i++) if (parsed.regions[i].name === nm) return parsed.regions[i];
    return null;
  }

  // -- paid: budgets ---------------------------------------------------
  function checkBudgets(parsed, budgets) {
    var rows = [], failed = 0;
    for (var i = 0; i < parsed.regions.length; i++) {
      var R = parsed.regions[i];
      var has = budgets && Object.prototype.hasOwnProperty.call(budgets, R.name);
      var b = has ? parseBudget(budgets[R.name], R.length) : null;
      var limit = b ? b.bytes : R.length;
      var ok = R.used <= limit;
      if (!ok) failed++;
      rows.push({ region: R.name, used: R.used, limit: limit, capacity: R.length,
                  budget: b ? b.text : '(region size)', over: ok ? 0 : R.used - limit,
                  headroom: limit - R.used, pct: limit ? R.used * 100 / limit : 0, ok: ok });
    }
    return { ok: failed === 0, failed: failed, rows: rows };
  }

  // -- paid: baseline diff ---------------------------------------------
  function diffMaps(base, head) {
    var regions = [], seen = {};
    function pushRegion(nm) {
      if (seen[nm]) return; seen[nm] = 1;
      var a = regionByName(base, nm), b = regionByName(head, nm);
      regions.push({
        name: nm, before: a ? a.used : 0, after: b ? b.used : 0,
        delta: (b ? b.used : 0) - (a ? a.used : 0),
        capacity: (b || a).length,
        wasOver: a ? a.over > 0 : false, isOver: b ? b.over > 0 : false,
        over: b ? b.over : 0
      });
    }
    for (var i = 0; i < base.regions.length; i++) pushRegion(base.regions[i].name);
    for (var j = 0; j < head.regions.length; j++) pushRegion(head.regions[j].name);

    // Only real MEMORY regions are compared. Debug sections (.debug_info,
    // .comment, .ARM.attributes) sit at address 0, belong to no region and
    // never reach the device. Counting them would report a 40 KB 'growth'
    // that costs the customer nothing - worse than saying nothing at all.
    var realRegions = {};
    for (var rr = 0; rr < regions.length; rr++) realRegions[regions[rr].name] = 1;

    // region -> object -> bytes. Nested, so no separator can collide with a
    // region or object name that contains spaces.
    function index(parsed) {
      var m = {};
      for (var k = 0; k < parsed.objects.length; k++) {
        var o = parsed.objects[k];
        for (var rg in o.byRegion) {
          if (!Object.prototype.hasOwnProperty.call(o.byRegion, rg)) continue;
          if (!realRegions[rg]) continue;
          if (!m[rg]) m[rg] = {};
          m[rg][o.name] = o.byRegion[rg];
        }
      }
      return m;
    }
    var A = index(base), B = index(head), objects = [];
    for (var rn = 0; rn < regions.length; rn++) {
      var rgn = regions[rn].name;
      var a = A[rgn] || {}, b = B[rgn] || {}, names = {};
      for (var na in a) if (Object.prototype.hasOwnProperty.call(a, na)) names[na] = 1;
      for (var nb in b) if (Object.prototype.hasOwnProperty.call(b, nb)) names[nb] = 1;
      for (var nm2 in names) {
        if (!Object.prototype.hasOwnProperty.call(names, nm2)) continue;
        var before = a[nm2] || 0, after = b[nm2] || 0;
        if (before === after) continue;
        objects.push({ region: rgn, name: nm2, before: before, after: after,
                       delta: after - before,
                       state: before === 0 ? 'added' : (after === 0 ? 'removed' : 'changed') });
      }
    }
    objects.sort(function (a, b) { return Math.abs(b.delta) - Math.abs(a.delta); });
    return { regions: regions, objects: objects };
  }

  // -- text report (the free answer) -----------------------------------
  function pad(s, w, right) {
    s = String(s);
    if (s.length >= w) return s;
    var sp = new Array(w - s.length + 1).join(' ');
    return right ? sp + s : s + sp;
  }

  function formatReport(parsed, opts) {
    opts = opts || {};
    var top = opts.top || 12;
    var L = [];
    L.push('Linker Map Auditor' + (parsed.name ? ' - ' + parsed.name : ''));
    L.push(parsed.stats.sections + ' output sections, ' + parsed.stats.inputs +
      ' input sections, ' + parsed.stats.objects + ' objects, ' +
      parsed.stats.archives + ' archives');
    L.push('');
    if (!parsed.regions.length) {
      L.push('No MEMORY regions found in this file.');
      for (var w = 0; w < parsed.warnings.length; w++) L.push('! ' + parsed.warnings[w]);
      return L.join('\n');
    }
    L.push(pad('REGION', 12) + pad('USED', 12, true) + pad('SIZE', 12, true) +
      pad('FREE', 12, true) + pad('USED%', 9, true) + '  STATUS');
    for (var i = 0; i < parsed.regions.length; i++) {
      var R = parsed.regions[i];
      L.push(pad(R.name, 12) + pad(commas(R.used), 12, true) + pad(commas(R.length), 12, true) +
        pad(commas(R.free), 12, true) + pad(R.pct.toFixed(1) + '%', 9, true) + '  ' +
        (R.over ? ('OVER by ' + commas(R.over) + ' B') : 'ok'));
    }
    for (var k = 0; k < parsed.regions.length; k++) {
      var Rg = parsed.regions[k];
      if (!Rg.used) continue;
      L.push('');
      L.push(Rg.name + ' - top spenders (' + humanBytes(Rg.used) + ' used)');
      var rows = topIn(parsed, Rg.name, top);
      for (var m = 0; m < rows.length; m++) {
        var sec = rows[m].sections;
        var secNames = Object.keys(sec).sort(function (a, b) { return sec[b] - sec[a]; })
          .slice(0, 3).join(', ');
        L.push('  ' + pad(commas(rows[m].size), 10, true) + '  ' + pad(rows[m].name, 34) + '  ' + secNames);
      }
      if (Rg.unattributed) {
        L.push('  ' + pad(commas(Rg.unattributed), 10, true) + '  ' + NOATTR_LABEL +
          ' (padding, heap/stack reservations)');
      }
    }
    for (var w2 = 0; w2 < parsed.warnings.length; w2++) L.push('! ' + parsed.warnings[w2]);
    return L.join('\n');
  }

  return {
    VERSION: VERSION,
    parseMap: parseMap,
    formatReport: formatReport,
    diffMaps: diffMaps,
    checkBudgets: checkBudgets,
    topIn: topIn,
    regionByName: regionByName,
    humanBytes: humanBytes,
    parseBudget: parseBudget,
    commas: commas
  };
}));
</script>
<script>
var EXAMPLES = {"over": {"label": "Build A - 16K/8K MCU, RAM overflows", "text": "Archive member included to satisfy reference by file (symbol)\n\nDrivers/libhal.a(stm32f4xx_hal.o)\n                              build/main.o (HAL_Init)\n/usr/lib/gcc/arm-none-eabi/newlib/libc_nano.a(lib_a-printf.o)\n                              build/log.o (printf)\n\nAllocating common symbols\nCommon symbol       size              file\n\napp_state           0x80              build/app.o\n\nDiscarded input sections\n\n .text           0x0000000000000000        0x0 build/main.o\n .data           0x0000000000000000        0x0 build/main.o\n\nMemory Configuration\n\nName             Origin             Length             Attributes\nFLASH            0x0000000008000000 0x0000000000010000 xr\nRAM              0x0000000020000000 0x0000000000002000 xrw\n*default*        0x0000000000000000 0xffffffffffffffff\n\nLinker script and memory map\n\nLOAD build/startup_stm32f0xx.o\nLOAD build/main.o\nLOAD build/app.o\nLOAD build/uart.o\nLOAD build/log.o\nLOAD build/system_stm32f4xx.o\nSTART GROUP\nLOAD Drivers/libhal.a\nLOAD /usr/lib/gcc/arm-none-eabi/newlib/libc_nano.a\nEND GROUP\n\n.isr_vector     0x0000000008000000      0x188\n *(.isr_vector)\n .isr_vector    0x0000000008000000      0x188 build/startup_stm32f0xx.o\n                0x0000000008000000                g_pfnVectors\n\n.text           0x0000000008000188     0x24d0\n *(.text)\n *(.text*)\n .text.main     0x0000000008000188       0xd4 build/main.o\n                0x0000000008000188                main\n .text.HAL_Init\n                0x000000000800025c       0x2c Drivers/libhal.a(stm32f4xx_hal.o)\n                0x000000000800025c                HAL_Init\n .text.HAL_RCC_OscConfig\n                0x0000000008000288      0x3a8 Drivers/libhal.a(stm32f4xx_hal_rcc.o)\n                0x0000000008000288                HAL_RCC_OscConfig\n .text.SystemInit\n                0x0000000008000630       0x68 build/system_stm32f4xx.o\n                0x0000000008000630                SystemInit\n .text.printf_core\n                0x0000000008000698     0x1f40 /usr/lib/gcc/arm-none-eabi/newlib/libc_nano.a(lib_a-printf.o)\n                0x0000000008000698                _printf_core\n *fill*         0x00000000080025d8        0x8\n .text.app_loop\n                0x00000000080025e0       0x54 build/app.o\n                0x00000000080025e0                app_loop\n                0x0000000008002658                . = ALIGN (0x4)\n\n.rodata         0x0000000008002658     0x1234\n *(.rodata)\n *(.rodata*)\n .rodata.str1.4\n                0x0000000008002658     0x1000 /usr/lib/gcc/arm-none-eabi/newlib/libc_nano.a(lib_a-printf.o)\n .rodata.table  0x0000000008003658      0x234 build/app.o\n                0x0000000008003658                lookup_table\n\n.data           0x0000000020000000      0x264 load address 0x000000000800388c\n                0x0000000020000000                _sdata = .\n *(.data)\n *(.data*)\n .data.cfg      0x0000000020000000      0x200 build/app.o\n                0x0000000020000000                g_cfg\n .data.tick     0x0000000020000200       0x64 build/main.o\n                0x0000000020000200                g_tick\n                0x0000000020000264                _edata = .\n\n.bss            0x0000000020000268     0x1a40\n                0x0000000020000268                _sbss = .\n *(.bss)\n *(.bss*)\n .bss.uart_rx_buf\n                0x0000000020000268     0x1000 build/uart.o\n                0x0000000020000268                uart_rx_buf\n .bss.log_pool  0x0000000020001268      0x800 build/log.o\n                0x0000000020001268                log_pool\n .bss.hal_state\n                0x0000000020001a68      0x1c0 Drivers/libhal.a(stm32f4xx_hal.o)\n                0x0000000020001a68                hal_state\n COMMON         0x0000000020001c28       0x80 build/app.o\n                0x0000000020001c28                app_state\n                0x0000000020001ca8                _ebss = .\n\n._user_heap_stack\n                0x0000000020001ca8      0x604\n                0x0000000020001ca8                . = ALIGN (0x8)\n                0x00000000200020ac                . = (. + _Min_Heap_Size)\n                0x00000000200022ac                . = (. + _Min_Stack_Size)\n\n.comment        0x0000000000000000       0x43\n .comment       0x0000000000000000       0x43 build/main.o\n\n.ARM.attributes\n                0x0000000000000000       0x33\n .ARM.attributes\n                0x0000000000000000       0x33 build/main.o\n\n.debug_info     0x0000000000000000     0x9c40\n .debug_info    0x0000000000000000     0x9c40 build/main.o\n\nOUTPUT(build/app.elf elf32-littlearm)\n"}, "base": {"label": "Build B - the same firmware one week earlier", "text": "Archive member included to satisfy reference by file (symbol)\n\nDrivers/libhal.a(stm32f4xx_hal.o)\n                              build/main.o (HAL_Init)\n/usr/lib/gcc/arm-none-eabi/newlib/libc_nano.a(lib_a-printf.o)\n                              build/log.o (printf)\n\nAllocating common symbols\nCommon symbol       size              file\n\napp_state           0x80              build/app.o\n\nMemory Configuration\n\nName             Origin             Length             Attributes\nFLASH            0x0000000008000000 0x0000000000010000 xr\nRAM              0x0000000020000000 0x0000000000002000 xrw\n*default*        0x0000000000000000 0xffffffffffffffff\n\nLinker script and memory map\n\nLOAD build/startup_stm32f0xx.o\nLOAD build/main.o\nSTART GROUP\nLOAD Drivers/libhal.a\nLOAD /usr/lib/gcc/arm-none-eabi/newlib/libc_nano.a\nEND GROUP\n\n.isr_vector     0x0000000008000000      0x188\n *(.isr_vector)\n .isr_vector    0x0000000008000000      0x188 build/startup_stm32f0xx.o\n                0x0000000008000000                g_pfnVectors\n\n.text           0x0000000008000188     0x20d0\n *(.text)\n *(.text*)\n .text.main     0x0000000008000188       0xd4 build/main.o\n                0x0000000008000188                main\n .text.HAL_Init\n                0x000000000800025c       0x2c Drivers/libhal.a(stm32f4xx_hal.o)\n                0x000000000800025c                HAL_Init\n .text.HAL_RCC_OscConfig\n                0x0000000008000288      0x3a8 Drivers/libhal.a(stm32f4xx_hal_rcc.o)\n                0x0000000008000288                HAL_RCC_OscConfig\n .text.SystemInit\n                0x0000000008000630       0x68 build/system_stm32f4xx.o\n                0x0000000008000630                SystemInit\n .text.printf_core\n                0x0000000008000698     0x1b40 /usr/lib/gcc/arm-none-eabi/newlib/libc_nano.a(lib_a-printf.o)\n                0x0000000008000698                _printf_core\n *fill*         0x00000000080021d8        0x8\n .text.app_loop\n                0x00000000080021e0       0x54 build/app.o\n                0x00000000080021e0                app_loop\n                0x0000000008002258                . = ALIGN (0x4)\n\n.rodata         0x0000000008002258     0x1234\n *(.rodata)\n *(.rodata*)\n .rodata.str1.4\n                0x0000000008002258     0x1000 /usr/lib/gcc/arm-none-eabi/newlib/libc_nano.a(lib_a-printf.o)\n .rodata.table  0x0000000008003258      0x234 build/app.o\n                0x0000000008003258                lookup_table\n\n.data           0x0000000020000000      0x264 load address 0x000000000800348c\n                0x0000000020000000                _sdata = .\n *(.data)\n *(.data*)\n .data.cfg      0x0000000020000000      0x200 build/app.o\n                0x0000000020000000                g_cfg\n .data.tick     0x0000000020000200       0x64 build/main.o\n                0x0000000020000200                g_tick\n                0x0000000020000264                _edata = .\n\n.bss            0x0000000020000268     0x1240\n                0x0000000020000268                _sbss = .\n *(.bss)\n *(.bss*)\n .bss.uart_rx_buf\n                0x0000000020000268      0x800 build/uart.o\n                0x0000000020000268                uart_rx_buf\n .bss.log_pool  0x0000000020000a68      0x800 build/log.o\n                0x0000000020000a68                log_pool\n .bss.hal_state\n                0x0000000020001268      0x1c0 Drivers/libhal.a(stm32f4xx_hal.o)\n                0x0000000020001268                hal_state\n COMMON         0x0000000020001428       0x80 build/app.o\n                0x0000000020001428                app_state\n                0x00000000200014a8                _ebss = .\n\n._user_heap_stack\n                0x00000000200014a8      0x604\n                0x00000000200014a8                . = ALIGN (0x8)\n                0x00000000200018a8                . = (. + _Min_Heap_Size)\n                0x0000000020001aac                . = (. + _Min_Stack_Size)\n\nOUTPUT(build/app.elf elf32-littlearm)\n"}, "fixed": {"label": "Build C - after shrinking the UART and log buffers", "text": "Archive member included to satisfy reference by file (symbol)\n\nDrivers/libhal.a(stm32f4xx_hal.o)\n                              build/main.o (HAL_Init)\n/usr/lib/gcc/arm-none-eabi/newlib/libc_nano.a(lib_a-printf.o)\n                              build/log.o (printf)\n\nAllocating common symbols\nCommon symbol       size              file\n\napp_state           0x80              build/app.o\n\nMemory Configuration\n\nName             Origin             Length             Attributes\nFLASH            0x0000000008000000 0x0000000000010000 xr\nRAM              0x0000000020000000 0x0000000000002000 xrw\n*default*        0x0000000000000000 0xffffffffffffffff\n\nLinker script and memory map\n\nLOAD build/startup_stm32f0xx.o\nLOAD build/main.o\nSTART GROUP\nLOAD Drivers/libhal.a\nLOAD /usr/lib/gcc/arm-none-eabi/newlib/libc_nano.a\nEND GROUP\n\n.isr_vector     0x0000000008000000      0x188\n *(.isr_vector)\n .isr_vector    0x0000000008000000      0x188 build/startup_stm32f0xx.o\n                0x0000000008000000                g_pfnVectors\n\n.text           0x0000000008000188     0x24d0\n *(.text)\n *(.text*)\n .text.main     0x0000000008000188       0xd4 build/main.o\n                0x0000000008000188                main\n .text.HAL_Init\n                0x000000000800025c       0x2c Drivers/libhal.a(stm32f4xx_hal.o)\n                0x000000000800025c                HAL_Init\n .text.HAL_RCC_OscConfig\n                0x0000000008000288      0x3a8 Drivers/libhal.a(stm32f4xx_hal_rcc.o)\n                0x0000000008000288                HAL_RCC_OscConfig\n .text.SystemInit\n                0x0000000008000630       0x68 build/system_stm32f4xx.o\n                0x0000000008000630                SystemInit\n .text.printf_core\n                0x0000000008000698     0x1f40 /usr/lib/gcc/arm-none-eabi/newlib/libc_nano.a(lib_a-printf.o)\n                0x0000000008000698                _printf_core\n *fill*         0x00000000080025d8        0x8\n .text.app_loop\n                0x00000000080025e0       0x54 build/app.o\n                0x00000000080025e0                app_loop\n                0x0000000008002658                . = ALIGN (0x4)\n\n.rodata         0x0000000008002658     0x1234\n *(.rodata)\n *(.rodata*)\n .rodata.str1.4\n                0x0000000008002658     0x1000 /usr/lib/gcc/arm-none-eabi/newlib/libc_nano.a(lib_a-printf.o)\n .rodata.table  0x0000000008003658      0x234 build/app.o\n                0x0000000008003658                lookup_table\n\n.data           0x0000000020000000      0x264 load address 0x000000000800388c\n                0x0000000020000000                _sdata = .\n *(.data)\n *(.data*)\n .data.cfg      0x0000000020000000      0x200 build/app.o\n                0x0000000020000000                g_cfg\n .data.tick     0x0000000020000200       0x64 build/main.o\n                0x0000000020000200                g_tick\n                0x0000000020000264                _edata = .\n\n.bss            0x0000000020000268      0xe40\n                0x0000000020000268                _sbss = .\n *(.bss)\n *(.bss*)\n .bss.uart_rx_buf\n                0x0000000020000268      0x800 build/uart.o\n                0x0000000020000268                uart_rx_buf\n .bss.log_pool  0x0000000020000a68      0x400 build/log.o\n                0x0000000020000a68                log_pool\n .bss.hal_state\n                0x0000000020000e68      0x1c0 Drivers/libhal.a(stm32f4xx_hal.o)\n                0x0000000020000e68                hal_state\n COMMON         0x0000000020001028       0x80 build/app.o\n                0x0000000020001028                app_state\n                0x00000000200010a8                _ebss = .\n\n._user_heap_stack\n                0x00000000200010a8      0x604\n                0x00000000200010a8                . = ALIGN (0x8)\n                0x00000000200014a8                . = (. + _Min_Heap_Size)\n                0x00000000200016ac                . = (. + _Min_Stack_Size)\n\nOUTPUT(build/app.elf elf32-littlearm)\n"}};
var $ = function (id) { return document.getElementById(id); };

function esc(s) {
  return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

function render(parsed) {
  var M = window.MapParse, H = [];
  if (!parsed.regions.length) {
    H.push('<p class="verdict bad">No MEMORY regions found</p>');
    H.push('<p>' + esc(parsed.warnings[0] || 'This does not look like a GNU ld map file.') + '</p>');
    $('result').innerHTML = H.join('');
    return;
  }
  var over = null;
  for (var i = 0; i < parsed.regions.length; i++) if (parsed.regions[i].over) over = parsed.regions[i];
  if (over) {
    H.push('<p class="verdict bad">' + esc(over.name) + ' overflowed by ' +
           M.commas(over.over) + ' bytes</p>');
  } else {
    var tight = parsed.regions[0];
    for (var t = 0; t < parsed.regions.length; t++) if (parsed.regions[t].pct > tight.pct) tight = parsed.regions[t];
    H.push('<p class="verdict ok">Fits - tightest region is ' + esc(tight.name) + ' at ' +
           tight.pct.toFixed(1) + '%, ' + M.commas(tight.free) + ' bytes free</p>');
  }

  H.push('<table><tr><th>Region</th><th>Used</th><th>Size</th><th>Free</th><th>Used %</th><th style="text-align:left">Fill</th></tr>');
  for (var r = 0; r < parsed.regions.length; r++) {
    var R = parsed.regions[r];
    var cls = R.over ? 'bad' : (R.pct >= 90 ? 'warn' : '');
    H.push('<tr><td>' + esc(R.name) + '</td><td>' + M.commas(R.used) + '</td><td>' + M.commas(R.length) +
      '</td><td>' + M.commas(R.free) + '</td><td>' + R.pct.toFixed(1) + '%</td>' +
      '<td style="text-align:left"><div class="bar"><i class="' + cls + '" style="width:' +
      Math.min(100, R.pct).toFixed(1) + '%"></i></div></td></tr>');
  }
  H.push('</table>');

  var top = parseInt($('topn').value, 10) || 12;
  H.push('<div class="grid" style="margin-top:18px">');
  for (var k = 0; k < parsed.regions.length; k++) {
    var Rg = parsed.regions[k];
    if (!Rg.used) continue;
    H.push('<div><h3>' + esc(Rg.name) + ' - who is spending it</h3><table>');
    H.push('<tr><th>Object</th><th>Bytes</th></tr>');
    var rows = M.topIn(parsed, Rg.name, top);
    for (var m = 0; m < rows.length; m++) {
      H.push('<tr><td class="name">' + esc(rows[m].name) + '</td><td>' + M.commas(rows[m].size) + '</td></tr>');
    }
    if (Rg.unattributed) {
      H.push('<tr><td class="name" style="color:#8b949e">not attributed to an input section</td><td style="color:#8b949e">' +
        M.commas(Rg.unattributed) + '</td></tr>');
    }
    H.push('</table></div>');
  }
  H.push('</div>');
  H.push('<p class="note">' + parsed.stats.sections + ' output sections, ' + parsed.stats.inputs +
    ' input sections, ' + parsed.stats.objects + ' objects, ' + parsed.stats.archives +
    ' archives. <code>.data</code> is charged twice on purpose: it occupies RAM at run time and its initialiser occupies FLASH.</p>');
  $('result').innerHTML = H.join('');
}

function run() {
  var txt = $('maptext').value;
  render(window.MapParse.parseMap(txt, 'pasted map'));
}

function loadExample() {
  var e = EXAMPLES[$('example').value];
  if (e) $('maptext').value = e.text;
  run();
}

$('run').addEventListener('click', run);
$('example').addEventListener('change', loadExample);
$('topn').addEventListener('change', run);
loadExample();
</script>


<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://marketplace.visualstudio.com/items?itemName=ReadyStack.linker-map-auditor" rel="noopener" data-yj-install-door="mp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'linker-map-auditor',src:'mp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">VS Code Marketplace</a><a href="https://open-vsx.org/extension/readystack/linker-map-auditor" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'linker-map-auditor',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">ext install ReadyStack.linker-map-auditor</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/cl/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get one email when this rule changes</b><div style="opacity:.78;margin:.35rem 0 .8rem">We watch the regulation and vendor sources behind Linker Map Auditor - FLASH &amp; RAM budget for GNU ld map files every day. When a rule changes, you get a single email with what changed and the updated check. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Notify me</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">You're on the list. One email when it changes.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"linker-map-auditor",lang:"en",source:"rule_alert"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);})();</script></section>

<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">Read the .map your linker already wrote and see, in exact bytes, which object file is spending your FLASH and RAM - and by how much you overflowed.</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">A freelance embedded software engineer averages $103.27/hour (contractrates.fyi crowdsourced rates, 2026); one afternoon spent hand-reading a map file costs more than this tool.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_2x41azB4vGEeUd5HdFkPGOiAorn6HYGk0WDro17qEb0" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
<!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button></div><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div><script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script>

<script data-yj="yj-use-beacon v2">(function(){try{var r=document.querySelector('main')||document.body;var p=location.pathname,k='yju:'+p,sent=false,armed=false;function ours(el){return !!(el&&el.closest&&el.closest('[data-yj],[data-yj-cta],.yj-email-gate,.yj-find,.yj-related,#yj-ask,#yjgo,#yjr'));}function fire(){if(sent||!armed||sessionStorage.getItem(k))return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}r.addEventListener('click',function(e){var b=e.target&&e.target.closest?e.target.closest('button,input[type=submit],[role=button]'):null;if(!b||ours(b))return;armed=true;setTimeout(fire,400);},true);r.addEventListener('input',function(e){if(!ours(e.target))armed=true;},true);var mo=new MutationObserver(function(){if(armed){fire();if(sent)mo.disconnect();}});mo.observe(r,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script>
<section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-09">Updated 2026-09-09</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-09","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku20171_result_card.jpg" alt="RAM 8,872 / 8,192 bytes: reading the one line your linker refuses to print" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>RAM 8,872 of 8,192 bytes, over by 680 - that is the complete answer a firmware engineer needs when arm-none-eabi-ld refuses to link, and it is the one line the linker never prints. What it prints instead is <code>region &#x27;RAM&#x27; overflowed by 680 bytes</code>, and then it stops. It tells you that you lost. It does not tell you who took it.</p>
<p>The information is not missing. It is sitting in <code>build/app.map</code>, the file your build already wrote, twenty thousand lines long, in hexadecimal, with section sizes on one line and the object file that owns them on another. Everything you need is public, in your own repository, and nobody looks.</p>
<h3>What is actually in a map file</h3>
<p>Three things, and they have to be combined:</p>
<ol><li>The <code>Memory Configuration</code> block: the real origin and length of every region.</li><li>Output sections, each with an address and a size, some with a separate <code>load address</code>.</li><li>Input sections, each naming the object file or archive member that contributed it.</li></ol>
<p>Combining them is the whole job, and it has two traps.</p>
<p><strong><code>.data</code> is charged twice.</strong> Its variables live in RAM at run time, and its initialiser sits in FLASH so it can be copied there at boot. A script that counts it once is wrong by exactly the size of your initialised globals. In the build above that is 612 bytes on both sides.</p>
<p><strong>Debug sections are charged to nothing.</strong> <code>.debug_info</code>, <code>.comment</code> and <code>.ARM.attributes</code> are laid down at address 0, inside no region, and never reach the device. Count them and a build that added debug symbols appears to have grown by forty kilobytes it did not grow by.</p>
<h3>The answer, for a real build</h3>
<pre><code>REGION              USED        SIZE        FREE     USED%  STATUS
FLASH             15,088      65,536      50,448     23.0%  ok
RAM                8,872       8,192        -680    108.3%  OVER by 680 B

RAM - top spenders
       4,096  uart.o                     .bss.uart_rx_buf
       2,048  log.o                      .bss.log_pool
         640  app.o                      .data.cfg, COMMON
         448  libhal.a(stm32f4xx_hal.o)  .bss.hal_state
         100  main.o                     .data.tick
       1,540  (not attributed)           padding, heap and stack reservations</code></pre>
<p>Now the fix is obvious rather than archaeological: one UART ring buffer is holding half of an 8 KB device. Halve it and the build fits with room to spare - 5,800 of 8,192 bytes, 70.8%.</p>
<p>Notice the last row. Alignment padding and the <code>_Min_Heap_Size</code> / <code>_Min_Stack_Size</code> reservations belong to no object file, so most hand-written scripts quietly drop them and their columns stop adding up. Here they get their own line, and the parts sum to the region total exactly: 7,332 attributed plus 1,540 unattributed is 8,872.</p>
<h3>Why not ask a model</h3>
<p>Because the task is adding twenty thousand hexadecimal numbers and grouping them by owner. A language model will give you a confident total, and you will have no way to tell that it drifted. This is arithmetic; it wants a parser, not a guess. Bloaty and puncover do it well but want the ELF and a matching toolchain; the <code>.map</code> file is the artefact you already have, including on a Windows machine with nothing installed.</p>
<h3>Try it on your own build</h3>
<p>Build with <code>-Wl,-Map=build/app.map</code>, then paste the file into the free web version, or open it in VS Code and run <strong>Linker Map: Analyze this linker map</strong>. Reading one map file is free, with no key and no limit. What one build cannot tell you is what changed since the build that fit - and that comparison, per-region budgets, and a CI-ready report are what the licence adds.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku20171_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku20171_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does Linker Map Auditor actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads the .map file GNU ld writes with -Wl,-Map and turns it into a byte-exact budget: every MEMORY region&#x27;s capacity, used bytes, free bytes and overflow, plus every object file, archive member and input section ranked by what it spends in that region. The .data section is charged to both FLASH and RAM, because both costs are real.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is Linker Map Auditor for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Embedded firmware engineers on microcontrollers with a fixed FLASH and RAM budget: STM32, nRF, ESP32, AVR, RISC-V, built with arm-none-eabi-gcc, STM32CubeIDE, MCUXpresso, Zephyr or PlatformIO. It is for the moment the linker prints region RAM overflowed by 680 bytes and you must find which of two hundred object files is responsible.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask an AI chatbot or use a free tool?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A chat model cannot reliably add twenty thousand hexadecimal sizes; it returns a confident total you cannot check. Bloaty and puncover need the ELF plus a matching toolchain, and MemBrowse is a CI service to wire up. The .map file is already on your disk, and this reads it in the editor already open, on Windows, with nothing installed.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free, and what needs a licence?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Analysing one map file is free forever: no key, no watermark, no file limit, the full region table and the full contributor ranking. The licence adds what a single build cannot show: saving a baseline, diffing a later build to see which object grew, per-region budgets that pass or fail, and JSON plus Markdown export for CI.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What would it cost to do this by hand?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Hand-reading a linker map means summing hexadecimal sizes and attributing them across a file routinely twenty thousand lines long. A freelance embedded software engineer averages $103.27 an hour on crowdsourced 2026 rate data, so one afternoon of that hunt costs several hundred dollars, and it repeats every time the firmware grows.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does Linker Map Auditor actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads the .map file GNU ld writes with -Wl,-Map and turns it into a byte-exact budget: every MEMORY region's capacity, used bytes, free bytes and overflow, plus every object file, archive member and input section ranked by what it spends in that region. The .data section is charged to both FLASH and RAM, because both costs are real."}}, {"@type": "Question", "name": "Who is Linker Map Auditor for?", "acceptedAnswer": {"@type": "Answer", "text": "Embedded firmware engineers on microcontrollers with a fixed FLASH and RAM budget: STM32, nRF, ESP32, AVR, RISC-V, built with arm-none-eabi-gcc, STM32CubeIDE, MCUXpresso, Zephyr or PlatformIO. It is for the moment the linker prints region RAM overflowed by 680 bytes and you must find which of two hundred object files is responsible."}}, {"@type": "Question", "name": "Why not just ask an AI chatbot or use a free tool?", "acceptedAnswer": {"@type": "Answer", "text": "A chat model cannot reliably add twenty thousand hexadecimal sizes; it returns a confident total you cannot check. Bloaty and puncover need the ELF plus a matching toolchain, and MemBrowse is a CI service to wire up. The .map file is already on your disk, and this reads it in the editor already open, on Windows, with nothing installed."}}, {"@type": "Question", "name": "What is free, and what needs a licence?", "acceptedAnswer": {"@type": "Answer", "text": "Analysing one map file is free forever: no key, no watermark, no file limit, the full region table and the full contributor ranking. The licence adds what a single build cannot show: saving a baseline, diffing a later build to see which object grew, per-region budgets that pass or fail, and JSON plus Markdown export for CI."}}, {"@type": "Question", "name": "What would it cost to do this by hand?", "acceptedAnswer": {"@type": "Answer", "text": "Hand-reading a linker map means summing hexadecimal sizes and attributing them across a file routinely twenty thousand lines long. A freelance embedded software engineer averages $103.27 an hour on crowdsourced 2026 rate data, so one afternoon of that hunt costs several hundred dollars, and it repeats every time the firmware grows."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:20171,slug:"linker-map-auditor",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/personal-data-map-dsar-audit"><span class="yj-rel-t">Personal Data Map: find every column a GDPR subject access request has to reac</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/bem-class-extractor-html"><span class="yj-rel-t">BEM Class Auditor for HTML and SCSS</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/firmware-release-gate"><span class="yj-rel-t">Firmware Release Gate for sdkconfig and prj.conf</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/gnu-global-cpp-vscode-config-pack"><span class="yj-rel-t">GNU Global C/C++ .vscode Config Pack</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/en/p/build-6-procreate-lettering-brushes-that-feel-like-ink-the-e"><span class="yj-rel-t">Build 6 Procreate Lettering Brushes That Feel Like Ink — The Exact-Settings Wo</span><span class="yj-rel-p">$39</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/ca-climate-disclosure-clock-2026"><span class="yj-rel-t">CA Climate Disclosure Clock 2026 — which SB 253 / SB 261 report your group owe</span><span class="yj-rel-p">$60</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body>
</html>