• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2015 the V8 project authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import { PROF_COLS, UNICODE_BLOCK } from "../src/constants";
6import { SelectionBroker } from "../src/selection-broker";
7import { TextView } from "../src/text-view";
8import { MySelection } from "./selection";
9import { anyToString, interpolate } from "./util";
10import { InstructionSelectionHandler } from "./selection-handler";
11
12const toolboxHTML = `<div id="disassembly-toolbox">
13<form>
14  <label><input id="show-instruction-address" type="checkbox" name="instruction-address">Show addresses</label>
15  <label><input id="show-instruction-binary" type="checkbox" name="instruction-binary">Show binary literal</label>
16  <label><input id="highlight-gap-instructions" type="checkbox" name="instruction-binary">Highlight gap instructions</label>
17</form>
18</div>`;
19
20export class DisassemblyView extends TextView {
21  SOURCE_POSITION_HEADER_REGEX: any;
22  addrEventCounts: any;
23  totalEventCounts: any;
24  maxEventCounts: any;
25  posLines: Array<any>;
26  instructionSelectionHandler: InstructionSelectionHandler;
27  offsetSelection: MySelection;
28  showInstructionAddressHandler: () => void;
29  showInstructionBinaryHandler: () => void;
30  highlightGapInstructionsHandler: () => void;
31
32  createViewElement() {
33    const pane = document.createElement('div');
34    pane.setAttribute('id', "disassembly");
35    pane.innerHTML =
36      `<pre id='disassembly-text-pre' class='prettyprint prettyprinted'>
37       <ul class='disassembly-list nolinenums noindent'>
38       </ul>
39     </pre>`;
40
41    return pane;
42  }
43
44  constructor(parentId, broker: SelectionBroker) {
45    super(parentId, broker);
46    const view = this;
47    const ADDRESS_STYLE = {
48      associateData: (text, fragment: HTMLElement) => {
49        const matches = text.match(/(?<address>0?x?[0-9a-fA-F]{8,16})(?<addressSpace>\s+)(?<offset>[0-9a-f]+)(?<offsetSpace>\s*)/);
50        const offset = Number.parseInt(matches.groups["offset"], 16);
51        const instructionKind = view.sourceResolver.getInstructionKindForPCOffset(offset);
52        fragment.dataset.instructionKind = instructionKind;
53        fragment.title = view.sourceResolver.instructionKindToReadableName(instructionKind);
54        const blockIds = view.sourceResolver.getBlockIdsForOffset(offset);
55        const blockIdElement = document.createElement("SPAN");
56        blockIdElement.className = "block-id com linkable-text";
57        blockIdElement.innerText = "";
58        if (blockIds && blockIds.length > 0) {
59          blockIds.forEach(blockId => view.addHtmlElementForBlockId(blockId, fragment));
60          blockIdElement.innerText = `B${blockIds.join(",")}:`;
61          blockIdElement.dataset.blockId = `${blockIds.join(",")}`;
62        }
63        fragment.appendChild(blockIdElement);
64        const addressElement = document.createElement("SPAN");
65        addressElement.className = "instruction-address";
66        addressElement.innerText = matches.groups["address"];
67        const offsetElement = document.createElement("SPAN");
68        offsetElement.innerText = matches.groups["offset"];
69        fragment.appendChild(addressElement);
70        fragment.appendChild(document.createTextNode(matches.groups["addressSpace"]));
71        fragment.appendChild(offsetElement);
72        fragment.appendChild(document.createTextNode(matches.groups["offsetSpace"]));
73        fragment.classList.add('tag');
74
75        if (!Number.isNaN(offset)) {
76          let pcOffset = view.sourceResolver.getKeyPcOffset(offset);
77          if (pcOffset == -1) pcOffset = Number(offset);
78          fragment.dataset.pcOffset = `${pcOffset}`;
79          addressElement.classList.add('linkable-text');
80          offsetElement.classList.add('linkable-text');
81        }
82        return true;
83      }
84    };
85    const UNCLASSIFIED_STYLE = {
86      css: 'com'
87    };
88    const NUMBER_STYLE = {
89      css: ['instruction-binary', 'lit']
90    };
91    const COMMENT_STYLE = {
92      css: 'com'
93    };
94    const OPCODE_ARGS = {
95      associateData: function (text, fragment) {
96        fragment.innerHTML = text;
97        const replacer = (match, hexOffset) => {
98          const offset = Number.parseInt(hexOffset, 16);
99          let keyOffset = view.sourceResolver.getKeyPcOffset(offset);
100          if (keyOffset == -1) keyOffset = Number(offset);
101          const blockIds = view.sourceResolver.getBlockIdsForOffset(offset);
102          let block = "";
103          let blockIdData = "";
104          if (blockIds && blockIds.length > 0) {
105            block = `B${blockIds.join(",")} `;
106            blockIdData = `data-block-id="${blockIds.join(",")}"`;
107          }
108          return `<span class="tag linkable-text" data-pc-offset="${keyOffset}" ${blockIdData}>${block}${match}</span>`;
109        };
110        const html = text.replace(/<.0?x?([0-9a-fA-F]+)>/g, replacer);
111        fragment.innerHTML = html;
112        return true;
113      }
114    };
115    const OPCODE_STYLE = {
116      css: 'kwd'
117    };
118    const BLOCK_HEADER_STYLE = {
119      associateData: function (text, fragment) {
120        if (view.sourceResolver.hasBlockStartInfo()) return false;
121        const matches = /\d+/.exec(text);
122        if (!matches) return true;
123        const blockId = matches[0];
124        fragment.dataset.blockId = blockId;
125        fragment.innerHTML = text;
126        fragment.className = "com block";
127        return true;
128      }
129    };
130    const SOURCE_POSITION_HEADER_STYLE = {
131      css: 'com'
132    };
133    view.SOURCE_POSITION_HEADER_REGEX = /^\s*--[^<]*<.*(not inlined|inlined\((\d+)\)):(\d+)>\s*--/;
134    const patterns = [
135      [
136        [/^0?x?[0-9a-fA-F]{8,16}\s+[0-9a-f]+\s+/, ADDRESS_STYLE, 1],
137        [view.SOURCE_POSITION_HEADER_REGEX, SOURCE_POSITION_HEADER_STYLE, -1],
138        [/^\s+-- B\d+ start.*/, BLOCK_HEADER_STYLE, -1],
139        [/^.*/, UNCLASSIFIED_STYLE, -1]
140      ],
141      [
142        [/^\s*[0-9a-f]+\s+/, NUMBER_STYLE, 2],
143        [/^\s*[0-9a-f]+\s+[0-9a-f]+\s+/, NUMBER_STYLE, 2],
144        [/^.*/, null, -1]
145      ],
146      [
147        [/^REX.W \S+\s+/, OPCODE_STYLE, 3],
148        [/^\S+\s+/, OPCODE_STYLE, 3],
149        [/^\S+$/, OPCODE_STYLE, -1],
150        [/^.*/, null, -1]
151      ],
152      [
153        [/^\s+/, null],
154        [/^[^;]+$/, OPCODE_ARGS, -1],
155        [/^[^;]+/, OPCODE_ARGS, 4],
156        [/^;/, COMMENT_STYLE, 5]
157      ],
158      [
159        [/^.+$/, COMMENT_STYLE, -1]
160      ]
161    ];
162    view.setPatterns(patterns);
163
164    const linkHandler = (e: MouseEvent) => {
165      if (!(e.target instanceof HTMLElement)) return;
166      const offsetAsString = typeof e.target.dataset.pcOffset != "undefined" ? e.target.dataset.pcOffset : e.target.parentElement.dataset.pcOffset;
167      const offset = Number.parseInt(offsetAsString, 10);
168      if ((typeof offsetAsString) != "undefined" && !Number.isNaN(offset)) {
169        view.offsetSelection.select([offset], true);
170        const nodes = view.sourceResolver.nodesForPCOffset(offset)[0];
171        if (nodes.length > 0) {
172          e.stopPropagation();
173          if (!e.shiftKey) {
174            view.selectionHandler.clear();
175          }
176          view.selectionHandler.select(nodes, true);
177        } else {
178          view.updateSelection();
179        }
180      }
181      return undefined;
182    };
183    view.divNode.addEventListener('click', linkHandler);
184
185    const linkHandlerBlock = e => {
186      const blockId = e.target.dataset.blockId;
187      if (typeof blockId != "undefined") {
188        const blockIds = blockId.split(",");
189        if (!e.shiftKey) {
190          view.selectionHandler.clear();
191        }
192        view.blockSelectionHandler.select(blockIds, true);
193      }
194    };
195    view.divNode.addEventListener('click', linkHandlerBlock);
196
197    this.offsetSelection = new MySelection(anyToString);
198    const instructionSelectionHandler = {
199      clear: function () {
200        view.offsetSelection.clear();
201        view.updateSelection();
202        broker.broadcastClear(instructionSelectionHandler);
203      },
204      select: function (instructionIds, selected) {
205        view.offsetSelection.select(instructionIds, selected);
206        view.updateSelection();
207        broker.broadcastBlockSelect(instructionSelectionHandler, instructionIds, selected);
208      },
209      brokeredInstructionSelect: function (instructionIds, selected) {
210        const firstSelect = view.offsetSelection.isEmpty();
211        const keyPcOffsets = view.sourceResolver.instructionsToKeyPcOffsets(instructionIds);
212        view.offsetSelection.select(keyPcOffsets, selected);
213        view.updateSelection(firstSelect);
214      },
215      brokeredClear: function () {
216        view.offsetSelection.clear();
217        view.updateSelection();
218      }
219    };
220    this.instructionSelectionHandler = instructionSelectionHandler;
221    broker.addInstructionHandler(instructionSelectionHandler);
222
223    const toolbox = document.createElement("div");
224    toolbox.id = "toolbox-anchor";
225    toolbox.innerHTML = toolboxHTML;
226    view.divNode.insertBefore(toolbox, view.divNode.firstChild);
227    const instructionAddressInput: HTMLInputElement = view.divNode.querySelector("#show-instruction-address");
228    const lastShowInstructionAddress = window.sessionStorage.getItem("show-instruction-address");
229    instructionAddressInput.checked = lastShowInstructionAddress == 'true';
230    const showInstructionAddressHandler = () => {
231      window.sessionStorage.setItem("show-instruction-address", `${instructionAddressInput.checked}`);
232      for (const el of view.divNode.querySelectorAll(".instruction-address")) {
233        el.classList.toggle("invisible", !instructionAddressInput.checked);
234      }
235    };
236    instructionAddressInput.addEventListener("change", showInstructionAddressHandler);
237    this.showInstructionAddressHandler = showInstructionAddressHandler;
238
239    const instructionBinaryInput: HTMLInputElement = view.divNode.querySelector("#show-instruction-binary");
240    const lastShowInstructionBinary = window.sessionStorage.getItem("show-instruction-binary");
241    instructionBinaryInput.checked = lastShowInstructionBinary == 'true';
242    const showInstructionBinaryHandler = () => {
243      window.sessionStorage.setItem("show-instruction-binary", `${instructionBinaryInput.checked}`);
244      for (const el of view.divNode.querySelectorAll(".instruction-binary")) {
245        el.classList.toggle("invisible", !instructionBinaryInput.checked);
246      }
247    };
248    instructionBinaryInput.addEventListener("change", showInstructionBinaryHandler);
249    this.showInstructionBinaryHandler = showInstructionBinaryHandler;
250
251    const highlightGapInstructionsInput: HTMLInputElement = view.divNode.querySelector("#highlight-gap-instructions");
252    const lastHighlightGapInstructions = window.sessionStorage.getItem("highlight-gap-instructions");
253    highlightGapInstructionsInput.checked = lastHighlightGapInstructions == 'true';
254    const highlightGapInstructionsHandler = () => {
255      window.sessionStorage.setItem("highlight-gap-instructions", `${highlightGapInstructionsInput.checked}`);
256      view.divNode.classList.toggle("highlight-gap-instructions", highlightGapInstructionsInput.checked);
257    };
258
259    highlightGapInstructionsInput.addEventListener("change", highlightGapInstructionsHandler);
260    this.highlightGapInstructionsHandler = highlightGapInstructionsHandler;
261  }
262
263  updateSelection(scrollIntoView: boolean = false) {
264    super.updateSelection(scrollIntoView);
265    const keyPcOffsets = this.sourceResolver.nodesToKeyPcOffsets(this.selection.selectedKeys());
266    if (this.offsetSelection) {
267      for (const key of this.offsetSelection.selectedKeys()) {
268        keyPcOffsets.push(Number(key));
269      }
270    }
271    for (const keyPcOffset of keyPcOffsets) {
272      const elementsToSelect = this.divNode.querySelectorAll(`[data-pc-offset='${keyPcOffset}']`);
273      for (const el of elementsToSelect) {
274        el.classList.toggle("selected", true);
275      }
276    }
277  }
278
279  initializeCode(sourceText, sourcePosition: number = 0) {
280    const view = this;
281    view.addrEventCounts = null;
282    view.totalEventCounts = null;
283    view.maxEventCounts = null;
284    view.posLines = new Array();
285    // Comment lines for line 0 include sourcePosition already, only need to
286    // add sourcePosition for lines > 0.
287    view.posLines[0] = sourcePosition;
288    if (sourceText && sourceText != "") {
289      const base = sourcePosition;
290      let current = 0;
291      const sourceLines = sourceText.split("\n");
292      for (let i = 1; i < sourceLines.length; i++) {
293        // Add 1 for newline character that is split off.
294        current += sourceLines[i - 1].length + 1;
295        view.posLines[i] = base + current;
296      }
297    }
298  }
299
300  initializePerfProfile(eventCounts) {
301    const view = this;
302    if (eventCounts !== undefined) {
303      view.addrEventCounts = eventCounts;
304
305      view.totalEventCounts = {};
306      view.maxEventCounts = {};
307      for (const evName in view.addrEventCounts) {
308        if (view.addrEventCounts.hasOwnProperty(evName)) {
309          const keys = Object.keys(view.addrEventCounts[evName]);
310          const values = keys.map(key => view.addrEventCounts[evName][key]);
311          view.totalEventCounts[evName] = values.reduce((a, b) => a + b);
312          view.maxEventCounts[evName] = values.reduce((a, b) => Math.max(a, b));
313        }
314      }
315    } else {
316      view.addrEventCounts = null;
317      view.totalEventCounts = null;
318      view.maxEventCounts = null;
319    }
320  }
321
322  showContent(data): void {
323    console.time("disassembly-view");
324    super.initializeContent(data, null);
325    this.showInstructionAddressHandler();
326    this.showInstructionBinaryHandler();
327    this.highlightGapInstructionsHandler();
328    console.timeEnd("disassembly-view");
329  }
330
331  // Shorten decimals and remove trailing zeroes for readability.
332  humanize(num) {
333    return num.toFixed(3).replace(/\.?0+$/, "") + "%";
334  }
335
336  processLine(line) {
337    const view = this;
338    let fragments = super.processLine(line);
339
340    // Add profiling data per instruction if available.
341    if (view.totalEventCounts) {
342      const matches = /^(0x[0-9a-fA-F]+)\s+\d+\s+[0-9a-fA-F]+/.exec(line);
343      if (matches) {
344        const newFragments = [];
345        for (const event in view.addrEventCounts) {
346          if (!view.addrEventCounts.hasOwnProperty(event)) continue;
347          const count = view.addrEventCounts[event][matches[1]];
348          let str = " ";
349          const cssCls = "prof";
350          if (count !== undefined) {
351            const perc = count / view.totalEventCounts[event] * 100;
352
353            let col = { r: 255, g: 255, b: 255 };
354            for (let i = 0; i < PROF_COLS.length; i++) {
355              if (perc === PROF_COLS[i].perc) {
356                col = PROF_COLS[i].col;
357                break;
358              } else if (perc > PROF_COLS[i].perc && perc < PROF_COLS[i + 1].perc) {
359                const col1 = PROF_COLS[i].col;
360                const col2 = PROF_COLS[i + 1].col;
361
362                const val = perc - PROF_COLS[i].perc;
363                const max = PROF_COLS[i + 1].perc - PROF_COLS[i].perc;
364
365                col.r = Math.round(interpolate(val, max, col1.r, col2.r));
366                col.g = Math.round(interpolate(val, max, col1.g, col2.g));
367                col.b = Math.round(interpolate(val, max, col1.b, col2.b));
368                break;
369              }
370            }
371
372            str = UNICODE_BLOCK;
373
374            const fragment = view.createFragment(str, cssCls);
375            fragment.title = event + ": " + view.humanize(perc) + " (" + count + ")";
376            fragment.style.color = "rgb(" + col.r + ", " + col.g + ", " + col.b + ")";
377
378            newFragments.push(fragment);
379          } else {
380            newFragments.push(view.createFragment(str, cssCls));
381          }
382        }
383        fragments = newFragments.concat(fragments);
384      }
385    }
386    return fragments;
387  }
388
389  detachSelection() { return null; }
390
391  public searchInputAction(searchInput: HTMLInputElement, e: Event, onlyVisible: boolean): void {
392    throw new Error("Method not implemented.");
393  }
394}
395