Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/browser/disassemblyView.ts
5252 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { PixelRatio } from '../../../../base/browser/pixelRatio.js';
7
import { $, Dimension, addStandardDisposableListener, append } from '../../../../base/browser/dom.js';
8
import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';
9
import { ITableContextMenuEvent, ITableRenderer, ITableVirtualDelegate } from '../../../../base/browser/ui/table/table.js';
10
import { binarySearch2 } from '../../../../base/common/arrays.js';
11
import { Color } from '../../../../base/common/color.js';
12
import { Emitter } from '../../../../base/common/event.js';
13
import { Disposable, IDisposable, dispose } from '../../../../base/common/lifecycle.js';
14
import { isAbsolute } from '../../../../base/common/path.js';
15
import { Constants } from '../../../../base/common/uint.js';
16
import { URI } from '../../../../base/common/uri.js';
17
import { applyFontInfo } from '../../../../editor/browser/config/domFontInfo.js';
18
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
19
import { BareFontInfo } from '../../../../editor/common/config/fontInfo.js';
20
import { createBareFontInfoFromRawSettings } from '../../../../editor/common/config/fontInfoFromSettings.js';
21
import { IRange, Range } from '../../../../editor/common/core/range.js';
22
import { StringBuilder } from '../../../../editor/common/core/stringBuilder.js';
23
import { ITextModel } from '../../../../editor/common/model.js';
24
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
25
import { localize } from '../../../../nls.js';
26
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
27
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
28
import { TextEditorSelectionRevealType } from '../../../../platform/editor/common/editor.js';
29
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
30
import { WorkbenchTable } from '../../../../platform/list/browser/listService.js';
31
import { ILogService } from '../../../../platform/log/common/log.js';
32
import { IStorageService } from '../../../../platform/storage/common/storage.js';
33
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
34
import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js';
35
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
36
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
37
import { EditorPane } from '../../../browser/parts/editor/editorPane.js';
38
import { IWorkbenchContribution } from '../../../common/contributions.js';
39
import { focusedStackFrameColor, topStackFrameColor } from './callStackEditorContribution.js';
40
import * as icons from './debugIcons.js';
41
import { CONTEXT_LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST, DISASSEMBLY_VIEW_ID, IDebugConfiguration, IDebugService, IDebugSession, IInstructionBreakpoint, State } from '../common/debug.js';
42
import { InstructionBreakpoint } from '../common/debugModel.js';
43
import { getUriFromSource } from '../common/debugSource.js';
44
import { isUriString, sourcesEqual } from '../common/debugUtils.js';
45
import { IEditorService } from '../../../services/editor/common/editorService.js';
46
import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';
47
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
48
import { IMenu, IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';
49
import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';
50
import { COPY_ADDRESS_ID, COPY_ADDRESS_LABEL } from '../../../../workbench/contrib/debug/browser/debugCommands.js';
51
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
52
import { getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
53
54
export interface IDisassembledInstructionEntry {
55
allowBreakpoint: boolean;
56
isBreakpointSet: boolean;
57
isBreakpointEnabled: boolean;
58
/** Instruction reference from the DA */
59
instructionReference: string;
60
/** Offset from the instructionReference that's the basis for the `instructionOffset` */
61
instructionReferenceOffset: number;
62
/** The number of instructions (+/-) away from the instructionReference and instructionReferenceOffset this instruction lies */
63
instructionOffset: number;
64
/** Whether this is the first instruction on the target line. */
65
showSourceLocation?: boolean;
66
/** Original instruction from the debugger */
67
instruction: DebugProtocol.DisassembledInstruction;
68
/** Parsed instruction address */
69
address: bigint;
70
}
71
72
// Special entry as a placeholer when disassembly is not available
73
const disassemblyNotAvailable: IDisassembledInstructionEntry = {
74
allowBreakpoint: false,
75
isBreakpointSet: false,
76
isBreakpointEnabled: false,
77
instructionReference: '',
78
instructionOffset: 0,
79
instructionReferenceOffset: 0,
80
address: 0n,
81
instruction: {
82
address: '-1',
83
instruction: localize('instructionNotAvailable', "Disassembly not available.")
84
},
85
};
86
87
export class DisassemblyView extends EditorPane {
88
89
private static readonly NUM_INSTRUCTIONS_TO_LOAD = 50;
90
91
// Used in instruction renderer
92
private _fontInfo: BareFontInfo | undefined;
93
private _disassembledInstructions: WorkbenchTable<IDisassembledInstructionEntry> | undefined;
94
private _onDidChangeStackFrame: Emitter<void>;
95
private _previousDebuggingState: State;
96
private _instructionBpList: readonly IInstructionBreakpoint[] = [];
97
private _enableSourceCodeRender: boolean = true;
98
private _loadingLock: boolean = false;
99
private readonly _referenceToMemoryAddress = new Map<string, bigint>();
100
private menu: IMenu;
101
102
constructor(
103
group: IEditorGroup,
104
@ITelemetryService telemetryService: ITelemetryService,
105
@IThemeService themeService: IThemeService,
106
@IStorageService storageService: IStorageService,
107
@IConfigurationService private readonly _configurationService: IConfigurationService,
108
@IInstantiationService private readonly _instantiationService: IInstantiationService,
109
@IDebugService private readonly _debugService: IDebugService,
110
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
111
@IMenuService menuService: IMenuService,
112
@IContextKeyService contextKeyService: IContextKeyService,
113
) {
114
super(DISASSEMBLY_VIEW_ID, group, telemetryService, themeService, storageService);
115
116
this.menu = menuService.createMenu(MenuId.DebugDisassemblyContext, contextKeyService);
117
this._register(this.menu);
118
this._disassembledInstructions = undefined;
119
this._onDidChangeStackFrame = this._register(new Emitter<void>({ leakWarningThreshold: 1000 }));
120
this._previousDebuggingState = _debugService.state;
121
this._register(_configurationService.onDidChangeConfiguration(e => {
122
if (e.affectsConfiguration('debug')) {
123
// show/hide source code requires changing height which WorkbenchTable doesn't support dynamic height, thus force a total reload.
124
const newValue = this._configurationService.getValue<IDebugConfiguration>('debug').disassemblyView.showSourceCode;
125
if (this._enableSourceCodeRender !== newValue) {
126
this._enableSourceCodeRender = newValue;
127
// todo: trigger rerender
128
} else {
129
this._disassembledInstructions?.rerender();
130
}
131
}
132
}));
133
}
134
135
get fontInfo() {
136
if (!this._fontInfo) {
137
this._fontInfo = this.createFontInfo();
138
139
this._register(this._configurationService.onDidChangeConfiguration(e => {
140
if (e.affectsConfiguration('editor')) {
141
this._fontInfo = this.createFontInfo();
142
}
143
}));
144
}
145
146
return this._fontInfo;
147
}
148
149
private createFontInfo() {
150
return createBareFontInfoFromRawSettings(this._configurationService.getValue('editor'), PixelRatio.getInstance(this.window).value);
151
}
152
153
get currentInstructionAddresses() {
154
return this._debugService.getModel().getSessions(false).
155
map(session => session.getAllThreads()).
156
reduce((prev, curr) => prev.concat(curr), []).
157
map(thread => thread.getTopStackFrame()).
158
map(frame => frame?.instructionPointerReference).
159
map(ref => ref ? this.getReferenceAddress(ref) : undefined);
160
}
161
162
// Instruction reference of the top stack frame of the focused stack
163
get focusedCurrentInstructionReference() {
164
return this._debugService.getViewModel().focusedStackFrame?.thread.getTopStackFrame()?.instructionPointerReference;
165
}
166
167
get focusedCurrentInstructionAddress() {
168
const ref = this.focusedCurrentInstructionReference;
169
return ref ? this.getReferenceAddress(ref) : undefined;
170
}
171
172
get focusedInstructionReference() {
173
return this._debugService.getViewModel().focusedStackFrame?.instructionPointerReference;
174
}
175
176
get focusedInstructionAddress() {
177
const ref = this.focusedInstructionReference;
178
return ref ? this.getReferenceAddress(ref) : undefined;
179
}
180
181
get isSourceCodeRender() { return this._enableSourceCodeRender; }
182
183
get debugSession(): IDebugSession | undefined {
184
return this._debugService.getViewModel().focusedSession;
185
}
186
187
get onDidChangeStackFrame() { return this._onDidChangeStackFrame.event; }
188
189
get focusedAddressAndOffset() {
190
const element = this._disassembledInstructions?.getFocusedElements()[0];
191
if (!element) {
192
return undefined;
193
}
194
195
return this.getAddressAndOffset(element);
196
}
197
198
getAddressAndOffset(element: IDisassembledInstructionEntry) {
199
const reference = element.instructionReference;
200
const offset = Number(element.address - this.getReferenceAddress(reference)!);
201
return { reference, offset, address: element.address };
202
}
203
204
protected createEditor(parent: HTMLElement): void {
205
this._enableSourceCodeRender = this._configurationService.getValue<IDebugConfiguration>('debug').disassemblyView.showSourceCode;
206
const lineHeight = this.fontInfo.lineHeight;
207
const thisOM = this;
208
const delegate = new class implements ITableVirtualDelegate<IDisassembledInstructionEntry> {
209
headerRowHeight: number = 0; // No header
210
getHeight(row: IDisassembledInstructionEntry): number {
211
if (thisOM.isSourceCodeRender && row.showSourceLocation && row.instruction.location?.path && row.instruction.line) {
212
// instruction line + source lines
213
if (row.instruction.endLine) {
214
return lineHeight * Math.max(2, (row.instruction.endLine - row.instruction.line + 2));
215
} else {
216
// source is only a single line.
217
return lineHeight * 2;
218
}
219
}
220
221
// just instruction line
222
return lineHeight;
223
}
224
};
225
226
const instructionRenderer = this._register(this._instantiationService.createInstance(InstructionRenderer, this));
227
228
this._disassembledInstructions = this._register(this._instantiationService.createInstance(WorkbenchTable,
229
'DisassemblyView', parent, delegate,
230
[
231
{
232
label: '',
233
tooltip: '',
234
weight: 0,
235
minimumWidth: this.fontInfo.lineHeight,
236
maximumWidth: this.fontInfo.lineHeight,
237
templateId: BreakpointRenderer.TEMPLATE_ID,
238
project(row: IDisassembledInstructionEntry): IDisassembledInstructionEntry { return row; }
239
},
240
{
241
label: localize('disassemblyTableColumnLabel', "instructions"),
242
tooltip: '',
243
weight: 0.3,
244
templateId: InstructionRenderer.TEMPLATE_ID,
245
project(row: IDisassembledInstructionEntry): IDisassembledInstructionEntry { return row; }
246
},
247
],
248
[
249
this._instantiationService.createInstance(BreakpointRenderer, this),
250
instructionRenderer,
251
],
252
{
253
identityProvider: { getId: (e: IDisassembledInstructionEntry) => e.instruction.address },
254
horizontalScrolling: false,
255
overrideStyles: {
256
listBackground: editorBackground
257
},
258
multipleSelectionSupport: false,
259
setRowLineHeight: false,
260
openOnSingleClick: false,
261
accessibilityProvider: new AccessibilityProvider(),
262
mouseSupport: false
263
}
264
)) as WorkbenchTable<IDisassembledInstructionEntry>;
265
266
this._disassembledInstructions.domNode.classList.add('disassembly-view');
267
268
if (this.focusedInstructionReference) {
269
this.reloadDisassembly(this.focusedInstructionReference, 0);
270
}
271
272
this._register(this._disassembledInstructions.onDidScroll(e => {
273
if (this._disassembledInstructions?.row(0) === disassemblyNotAvailable) {
274
return;
275
}
276
if (this._loadingLock) {
277
return;
278
}
279
280
if (e.oldScrollTop > e.scrollTop && e.scrollTop < e.height) {
281
this._loadingLock = true;
282
const prevTop = Math.floor(e.scrollTop / this.fontInfo.lineHeight);
283
this.scrollUp_LoadDisassembledInstructions(DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD).then((loaded) => {
284
if (loaded > 0) {
285
this._disassembledInstructions!.reveal(prevTop + loaded, 0);
286
}
287
}).finally(() => { this._loadingLock = false; });
288
} else if (e.oldScrollTop < e.scrollTop && e.scrollTop + e.height > e.scrollHeight - e.height) {
289
this._loadingLock = true;
290
this.scrollDown_LoadDisassembledInstructions(DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD).finally(() => { this._loadingLock = false; });
291
}
292
}));
293
294
this._register(this._disassembledInstructions.onContextMenu(e => this.onContextMenu(e)));
295
296
this._register(this._debugService.getViewModel().onDidFocusStackFrame(({ stackFrame }) => {
297
if (this._disassembledInstructions && stackFrame?.instructionPointerReference) {
298
this.goToInstructionAndOffset(stackFrame.instructionPointerReference, 0);
299
}
300
this._onDidChangeStackFrame.fire();
301
}));
302
303
// refresh breakpoints view
304
this._register(this._debugService.getModel().onDidChangeBreakpoints(bpEvent => {
305
if (bpEvent && this._disassembledInstructions) {
306
// draw viewable BP
307
let changed = false;
308
bpEvent.added?.forEach((bp) => {
309
if (bp instanceof InstructionBreakpoint) {
310
const index = this.getIndexFromReferenceAndOffset(bp.instructionReference, bp.offset);
311
if (index >= 0) {
312
this._disassembledInstructions!.row(index).isBreakpointSet = true;
313
this._disassembledInstructions!.row(index).isBreakpointEnabled = bp.enabled;
314
changed = true;
315
}
316
}
317
});
318
319
bpEvent.removed?.forEach((bp) => {
320
if (bp instanceof InstructionBreakpoint) {
321
const index = this.getIndexFromReferenceAndOffset(bp.instructionReference, bp.offset);
322
if (index >= 0) {
323
this._disassembledInstructions!.row(index).isBreakpointSet = false;
324
changed = true;
325
}
326
}
327
});
328
329
bpEvent.changed?.forEach((bp) => {
330
if (bp instanceof InstructionBreakpoint) {
331
const index = this.getIndexFromReferenceAndOffset(bp.instructionReference, bp.offset);
332
if (index >= 0) {
333
if (this._disassembledInstructions!.row(index).isBreakpointEnabled !== bp.enabled) {
334
this._disassembledInstructions!.row(index).isBreakpointEnabled = bp.enabled;
335
changed = true;
336
}
337
}
338
}
339
});
340
341
// get an updated list so that items beyond the current range would render when reached.
342
this._instructionBpList = this._debugService.getModel().getInstructionBreakpoints();
343
344
// breakpoints restored from a previous session can be based on memory
345
// references that may no longer exist in the current session. Request
346
// those instructions to be loaded so the BP can be displayed.
347
for (const bp of this._instructionBpList) {
348
this.primeMemoryReference(bp.instructionReference);
349
}
350
351
if (changed) {
352
this._onDidChangeStackFrame.fire();
353
}
354
}
355
}));
356
357
this._register(this._debugService.onDidChangeState(e => {
358
if ((e === State.Running || e === State.Stopped) &&
359
(this._previousDebuggingState !== State.Running && this._previousDebuggingState !== State.Stopped)) {
360
// Just started debugging, clear the view
361
this.clear();
362
this._enableSourceCodeRender = this._configurationService.getValue<IDebugConfiguration>('debug').disassemblyView.showSourceCode;
363
}
364
365
this._previousDebuggingState = e;
366
this._onDidChangeStackFrame.fire();
367
}));
368
}
369
370
layout(dimension: Dimension): void {
371
this._disassembledInstructions?.layout(dimension.height);
372
}
373
374
async goToInstructionAndOffset(instructionReference: string, offset: number, focus?: boolean) {
375
let addr = this._referenceToMemoryAddress.get(instructionReference);
376
if (addr === undefined) {
377
await this.loadDisassembledInstructions(instructionReference, 0, -DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD, DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD * 2);
378
addr = this._referenceToMemoryAddress.get(instructionReference);
379
}
380
381
if (addr) {
382
this.goToAddress(addr + BigInt(offset), focus);
383
}
384
}
385
386
/** Gets the address associated with the instruction reference. */
387
getReferenceAddress(instructionReference: string) {
388
return this._referenceToMemoryAddress.get(instructionReference);
389
}
390
391
/**
392
* Go to the address provided. If no address is provided, reveal the address of the currently focused stack frame. Returns false if that address is not available.
393
*/
394
private goToAddress(address: bigint, focus?: boolean): boolean {
395
if (!this._disassembledInstructions) {
396
return false;
397
}
398
399
if (!address) {
400
return false;
401
}
402
403
const index = this.getIndexFromAddress(address);
404
if (index >= 0) {
405
this._disassembledInstructions.reveal(index);
406
407
if (focus) {
408
this._disassembledInstructions.domFocus();
409
this._disassembledInstructions.setFocus([index]);
410
}
411
return true;
412
}
413
414
return false;
415
}
416
417
private async scrollUp_LoadDisassembledInstructions(instructionCount: number): Promise<number> {
418
const first = this._disassembledInstructions?.row(0);
419
if (first) {
420
return this.loadDisassembledInstructions(
421
first.instructionReference,
422
first.instructionReferenceOffset,
423
first.instructionOffset - instructionCount,
424
instructionCount,
425
);
426
}
427
428
return 0;
429
}
430
431
private async scrollDown_LoadDisassembledInstructions(instructionCount: number): Promise<number> {
432
const last = this._disassembledInstructions?.row(this._disassembledInstructions?.length - 1);
433
if (last) {
434
return this.loadDisassembledInstructions(
435
last.instructionReference,
436
last.instructionReferenceOffset,
437
last.instructionOffset + 1,
438
instructionCount,
439
);
440
}
441
442
return 0;
443
}
444
445
/**
446
* Sets the memory reference address. We don't just loadDisassembledInstructions
447
* for this, since we can't really deal with discontiguous ranges (we can't
448
* detect _if_ a range is discontiguous since we don't know how much memory
449
* comes between instructions.)
450
*/
451
private async primeMemoryReference(instructionReference: string) {
452
if (this._referenceToMemoryAddress.has(instructionReference)) {
453
return true;
454
}
455
456
const s = await this.debugSession?.disassemble(instructionReference, 0, 0, 1);
457
if (s && s.length > 0) {
458
try {
459
this._referenceToMemoryAddress.set(instructionReference, BigInt(s[0].address));
460
return true;
461
} catch {
462
return false;
463
}
464
}
465
466
return false;
467
}
468
469
/** Loads disasembled instructions. Returns the number of instructions that were loaded. */
470
private async loadDisassembledInstructions(instructionReference: string, offset: number, instructionOffset: number, instructionCount: number): Promise<number> {
471
const session = this.debugSession;
472
const resultEntries = await session?.disassemble(instructionReference, offset, instructionOffset, instructionCount);
473
474
// Ensure we always load the baseline instructions so we know what address the instructionReference refers to.
475
if (!this._referenceToMemoryAddress.has(instructionReference) && instructionOffset !== 0) {
476
await this.loadDisassembledInstructions(instructionReference, 0, 0, DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD);
477
}
478
479
if (session && resultEntries && this._disassembledInstructions) {
480
const newEntries: IDisassembledInstructionEntry[] = [];
481
482
let lastLocation: DebugProtocol.Source | undefined;
483
let lastLine: IRange | undefined;
484
for (let i = 0; i < resultEntries.length; i++) {
485
const instruction = resultEntries[i];
486
const thisInstructionOffset = instructionOffset + i;
487
488
// Forward fill the missing location as detailed in the DAP spec.
489
if (instruction.location) {
490
lastLocation = instruction.location;
491
lastLine = undefined;
492
}
493
494
if (instruction.line) {
495
const currentLine: IRange = {
496
startLineNumber: instruction.line,
497
startColumn: instruction.column ?? 0,
498
endLineNumber: instruction.endLine ?? instruction.line,
499
endColumn: instruction.endColumn ?? 0,
500
};
501
502
// Add location only to the first unique range. This will give the appearance of grouping of instructions.
503
if (!Range.equalsRange(currentLine, lastLine ?? null)) {
504
lastLine = currentLine;
505
instruction.location = lastLocation;
506
}
507
}
508
509
let address: bigint;
510
try {
511
address = BigInt(instruction.address);
512
} catch {
513
console.error(`Could not parse disassembly address ${instruction.address} (in ${JSON.stringify(instruction)})`);
514
continue;
515
}
516
517
if (address === -1n) {
518
// Ignore invalid instructions returned by the adapter.
519
continue;
520
}
521
522
const entry: IDisassembledInstructionEntry = {
523
allowBreakpoint: true,
524
isBreakpointSet: false,
525
isBreakpointEnabled: false,
526
instructionReference,
527
instructionReferenceOffset: offset,
528
instructionOffset: thisInstructionOffset,
529
instruction,
530
address,
531
};
532
533
newEntries.push(entry);
534
535
// if we just loaded the first instruction for this reference, mark its address.
536
if (offset === 0 && thisInstructionOffset === 0) {
537
this._referenceToMemoryAddress.set(instructionReference, address);
538
}
539
}
540
541
if (newEntries.length === 0) {
542
return 0;
543
}
544
545
const refBaseAddress = this._referenceToMemoryAddress.get(instructionReference);
546
const bps = this._instructionBpList.map(p => {
547
const base = this._referenceToMemoryAddress.get(p.instructionReference);
548
if (!base) {
549
return undefined;
550
}
551
return {
552
enabled: p.enabled,
553
address: base + BigInt(p.offset || 0),
554
};
555
});
556
557
if (refBaseAddress !== undefined) {
558
for (const entry of newEntries) {
559
const bp = bps.find(p => p?.address === entry.address);
560
if (bp) {
561
entry.isBreakpointSet = true;
562
entry.isBreakpointEnabled = bp.enabled;
563
}
564
}
565
}
566
567
const da = this._disassembledInstructions;
568
if (da.length === 1 && this._disassembledInstructions.row(0) === disassemblyNotAvailable) {
569
da.splice(0, 1);
570
}
571
572
const firstAddr = newEntries[0].address;
573
const lastAddr = newEntries[newEntries.length - 1].address;
574
575
const startN = binarySearch2(da.length, i => Number(da.row(i).address - firstAddr));
576
const start = startN < 0 ? ~startN : startN;
577
const endN = binarySearch2(da.length, i => Number(da.row(i).address - lastAddr));
578
const end = endN < 0 ? ~endN : endN + 1;
579
const toDelete = end - start;
580
581
// Go through everything we're about to add, and only show the source
582
// location if it's different from the previous one, "grouping" instructions by line
583
let lastLocated: undefined | DebugProtocol.DisassembledInstruction;
584
for (let i = start - 1; i >= 0; i--) {
585
const { instruction } = da.row(i);
586
if (instruction.location && instruction.line !== undefined) {
587
lastLocated = instruction;
588
break;
589
}
590
}
591
592
const shouldShowLocation = (instruction: DebugProtocol.DisassembledInstruction) =>
593
instruction.line !== undefined && instruction.location !== undefined &&
594
(!lastLocated || !sourcesEqual(instruction.location, lastLocated.location) || instruction.line !== lastLocated.line);
595
596
for (const entry of newEntries) {
597
if (shouldShowLocation(entry.instruction)) {
598
entry.showSourceLocation = true;
599
lastLocated = entry.instruction;
600
}
601
}
602
603
da.splice(start, toDelete, newEntries);
604
605
return newEntries.length - toDelete;
606
}
607
608
return 0;
609
}
610
611
private getIndexFromReferenceAndOffset(instructionReference: string, offset: number): number {
612
const addr = this._referenceToMemoryAddress.get(instructionReference);
613
if (addr === undefined) {
614
return -1;
615
}
616
617
return this.getIndexFromAddress(addr + BigInt(offset));
618
}
619
620
private getIndexFromAddress(address: bigint): number {
621
const disassembledInstructions = this._disassembledInstructions;
622
if (disassembledInstructions && disassembledInstructions.length > 0) {
623
return binarySearch2(disassembledInstructions.length, index => {
624
const row = disassembledInstructions.row(index);
625
return Number(row.address - address);
626
});
627
}
628
629
return -1;
630
}
631
632
/**
633
* Clears the table and reload instructions near the target address
634
*/
635
private reloadDisassembly(instructionReference: string, offset: number) {
636
if (!this._disassembledInstructions) {
637
return;
638
}
639
640
this._loadingLock = true; // stop scrolling during the load.
641
this.clear();
642
this._instructionBpList = this._debugService.getModel().getInstructionBreakpoints();
643
this.loadDisassembledInstructions(instructionReference, offset, -DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD * 4, DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD * 8).then(() => {
644
// on load, set the target instruction as the current instructionReference.
645
if (this._disassembledInstructions!.length > 0) {
646
let targetIndex: number | undefined = undefined;
647
const refBaseAddress = this._referenceToMemoryAddress.get(instructionReference);
648
if (refBaseAddress !== undefined) {
649
const da = this._disassembledInstructions!;
650
targetIndex = binarySearch2(da.length, i => Number(da.row(i).address - refBaseAddress));
651
if (targetIndex < 0) {
652
targetIndex = ~targetIndex; // shouldn't happen, but fail gracefully if it does
653
}
654
}
655
656
// If didn't find the instructonReference, set the target instruction in the middle of the page.
657
if (targetIndex === undefined) {
658
targetIndex = Math.floor(this._disassembledInstructions!.length / 2);
659
}
660
661
this._disassembledInstructions!.reveal(targetIndex, 0.5);
662
663
// Always focus the target address on reload, or arrow key navigation would look terrible
664
this._disassembledInstructions!.domFocus();
665
this._disassembledInstructions!.setFocus([targetIndex]);
666
}
667
this._loadingLock = false;
668
});
669
}
670
671
private clear() {
672
this._referenceToMemoryAddress.clear();
673
this._disassembledInstructions?.splice(0, this._disassembledInstructions.length, [disassemblyNotAvailable]);
674
}
675
676
private onContextMenu(e: ITableContextMenuEvent<IDisassembledInstructionEntry>): void {
677
const actions = getFlatContextMenuActions(this.menu.getActions({ shouldForwardArgs: true }));
678
this._contextMenuService.showContextMenu({
679
getAnchor: () => e.anchor,
680
getActions: () => actions,
681
getActionsContext: () => e.element
682
});
683
}
684
}
685
686
interface IBreakpointColumnTemplateData {
687
currentElement: { element?: IDisassembledInstructionEntry };
688
icon: HTMLElement;
689
disposables: IDisposable[];
690
}
691
692
class BreakpointRenderer implements ITableRenderer<IDisassembledInstructionEntry, IBreakpointColumnTemplateData> {
693
694
static readonly TEMPLATE_ID = 'breakpoint';
695
696
templateId: string = BreakpointRenderer.TEMPLATE_ID;
697
698
private readonly _breakpointIcon = 'codicon-' + icons.breakpoint.regular.id;
699
private readonly _breakpointDisabledIcon = 'codicon-' + icons.breakpoint.disabled.id;
700
private readonly _breakpointHintIcon = 'codicon-' + icons.debugBreakpointHint.id;
701
private readonly _debugStackframe = 'codicon-' + icons.debugStackframe.id;
702
private readonly _debugStackframeFocused = 'codicon-' + icons.debugStackframeFocused.id;
703
704
constructor(
705
private readonly _disassemblyView: DisassemblyView,
706
@IDebugService private readonly _debugService: IDebugService
707
) {
708
}
709
710
renderTemplate(container: HTMLElement): IBreakpointColumnTemplateData {
711
// align from the bottom so that it lines up with instruction when source code is present.
712
container.style.alignSelf = 'flex-end';
713
714
const icon = append(container, $('.codicon'));
715
icon.style.display = 'flex';
716
icon.style.alignItems = 'center';
717
icon.style.justifyContent = 'center';
718
icon.style.height = this._disassemblyView.fontInfo.lineHeight + 'px';
719
720
const currentElement: { element?: IDisassembledInstructionEntry } = { element: undefined };
721
722
const disposables = [
723
this._disassemblyView.onDidChangeStackFrame(() => this.rerenderDebugStackframe(icon, currentElement.element)),
724
addStandardDisposableListener(container, 'mouseover', () => {
725
if (currentElement.element?.allowBreakpoint) {
726
icon.classList.add(this._breakpointHintIcon);
727
}
728
}),
729
addStandardDisposableListener(container, 'mouseout', () => {
730
if (currentElement.element?.allowBreakpoint) {
731
icon.classList.remove(this._breakpointHintIcon);
732
}
733
}),
734
addStandardDisposableListener(container, 'click', () => {
735
if (currentElement.element?.allowBreakpoint) {
736
// click show hint while waiting for BP to resolve.
737
icon.classList.add(this._breakpointHintIcon);
738
const reference = currentElement.element.instructionReference;
739
const offset = Number(currentElement.element.address - this._disassemblyView.getReferenceAddress(reference)!);
740
if (currentElement.element.isBreakpointSet) {
741
this._debugService.removeInstructionBreakpoints(reference, offset);
742
} else if (currentElement.element.allowBreakpoint && !currentElement.element.isBreakpointSet) {
743
this._debugService.addInstructionBreakpoint({ instructionReference: reference, offset, address: currentElement.element.address, canPersist: false });
744
}
745
}
746
})
747
];
748
749
return { currentElement, icon, disposables };
750
}
751
752
renderElement(element: IDisassembledInstructionEntry, index: number, templateData: IBreakpointColumnTemplateData): void {
753
templateData.currentElement.element = element;
754
this.rerenderDebugStackframe(templateData.icon, element);
755
}
756
757
disposeTemplate(templateData: IBreakpointColumnTemplateData): void {
758
dispose(templateData.disposables);
759
templateData.disposables = [];
760
}
761
762
private rerenderDebugStackframe(icon: HTMLElement, element?: IDisassembledInstructionEntry) {
763
if (element?.address === this._disassemblyView.focusedCurrentInstructionAddress) {
764
icon.classList.add(this._debugStackframe);
765
} else if (element?.address === this._disassemblyView.focusedInstructionAddress) {
766
icon.classList.add(this._debugStackframeFocused);
767
} else {
768
icon.classList.remove(this._debugStackframe);
769
icon.classList.remove(this._debugStackframeFocused);
770
}
771
772
icon.classList.remove(this._breakpointHintIcon);
773
774
if (element?.isBreakpointSet) {
775
if (element.isBreakpointEnabled) {
776
icon.classList.add(this._breakpointIcon);
777
icon.classList.remove(this._breakpointDisabledIcon);
778
} else {
779
icon.classList.remove(this._breakpointIcon);
780
icon.classList.add(this._breakpointDisabledIcon);
781
}
782
} else {
783
icon.classList.remove(this._breakpointIcon);
784
icon.classList.remove(this._breakpointDisabledIcon);
785
}
786
}
787
}
788
789
interface IInstructionColumnTemplateData {
790
currentElement: { element?: IDisassembledInstructionEntry };
791
// TODO: hover widget?
792
instruction: HTMLElement;
793
sourcecode: HTMLElement;
794
// disposed when cell is closed.
795
cellDisposable: IDisposable[];
796
// disposed when template is closed.
797
disposables: IDisposable[];
798
}
799
800
class InstructionRenderer extends Disposable implements ITableRenderer<IDisassembledInstructionEntry, IInstructionColumnTemplateData> {
801
802
static readonly TEMPLATE_ID = 'instruction';
803
804
private static readonly INSTRUCTION_ADDR_MIN_LENGTH = 25;
805
private static readonly INSTRUCTION_BYTES_MIN_LENGTH = 30;
806
807
templateId: string = InstructionRenderer.TEMPLATE_ID;
808
809
private _topStackFrameColor: Color | undefined;
810
private _focusedStackFrameColor: Color | undefined;
811
812
constructor(
813
private readonly _disassemblyView: DisassemblyView,
814
@IThemeService themeService: IThemeService,
815
@IEditorService private readonly editorService: IEditorService,
816
@ITextModelService private readonly textModelService: ITextModelService,
817
@IUriIdentityService private readonly uriService: IUriIdentityService,
818
@ILogService private readonly logService: ILogService,
819
) {
820
super();
821
822
this._topStackFrameColor = themeService.getColorTheme().getColor(topStackFrameColor);
823
this._focusedStackFrameColor = themeService.getColorTheme().getColor(focusedStackFrameColor);
824
825
this._register(themeService.onDidColorThemeChange(e => {
826
this._topStackFrameColor = e.getColor(topStackFrameColor);
827
this._focusedStackFrameColor = e.getColor(focusedStackFrameColor);
828
}));
829
}
830
831
renderTemplate(container: HTMLElement): IInstructionColumnTemplateData {
832
const sourcecode = append(container, $('.sourcecode'));
833
const instruction = append(container, $('.instruction'));
834
this.applyFontInfo(sourcecode);
835
this.applyFontInfo(instruction);
836
const currentElement: { element?: IDisassembledInstructionEntry } = { element: undefined };
837
const cellDisposable: IDisposable[] = [];
838
839
const disposables = [
840
this._disassemblyView.onDidChangeStackFrame(() => this.rerenderBackground(instruction, sourcecode, currentElement.element)),
841
addStandardDisposableListener(sourcecode, 'dblclick', () => this.openSourceCode(currentElement.element?.instruction)),
842
];
843
844
return { currentElement, instruction, sourcecode, cellDisposable, disposables };
845
}
846
847
renderElement(element: IDisassembledInstructionEntry, index: number, templateData: IInstructionColumnTemplateData): void {
848
this.renderElementInner(element, index, templateData);
849
}
850
851
private async renderElementInner(element: IDisassembledInstructionEntry, index: number, templateData: IInstructionColumnTemplateData): Promise<void> {
852
templateData.currentElement.element = element;
853
const instruction = element.instruction;
854
templateData.sourcecode.innerText = '';
855
const sb = new StringBuilder(1000);
856
857
if (this._disassemblyView.isSourceCodeRender && element.showSourceLocation && instruction.location?.path && instruction.line !== undefined) {
858
const sourceURI = this.getUriFromSource(instruction);
859
860
if (sourceURI) {
861
let textModel: ITextModel | undefined = undefined;
862
const sourceSB = new StringBuilder(10000);
863
const ref = await this.textModelService.createModelReference(sourceURI);
864
if (templateData.currentElement.element !== element) {
865
return; // avoid a race, #192831
866
}
867
textModel = ref.object.textEditorModel;
868
templateData.cellDisposable.push(ref);
869
870
// templateData could have moved on during async. Double check if it is still the same source.
871
if (textModel && templateData.currentElement.element === element) {
872
let lineNumber = instruction.line;
873
874
while (lineNumber && lineNumber >= 1 && lineNumber <= textModel.getLineCount()) {
875
const lineContent = textModel.getLineContent(lineNumber);
876
sourceSB.appendString(` ${lineNumber}: `);
877
sourceSB.appendString(lineContent + '\n');
878
879
if (instruction.endLine && lineNumber < instruction.endLine) {
880
lineNumber++;
881
continue;
882
}
883
884
break;
885
}
886
887
templateData.sourcecode.innerText = sourceSB.build();
888
}
889
}
890
}
891
892
let spacesToAppend = 10;
893
894
if (instruction.address !== '-1') {
895
sb.appendString(instruction.address);
896
if (instruction.address.length < InstructionRenderer.INSTRUCTION_ADDR_MIN_LENGTH) {
897
spacesToAppend = InstructionRenderer.INSTRUCTION_ADDR_MIN_LENGTH - instruction.address.length;
898
}
899
for (let i = 0; i < spacesToAppend; i++) {
900
sb.appendString(' ');
901
}
902
}
903
904
if (instruction.instructionBytes) {
905
sb.appendString(instruction.instructionBytes);
906
spacesToAppend = 10;
907
if (instruction.instructionBytes.length < InstructionRenderer.INSTRUCTION_BYTES_MIN_LENGTH) {
908
spacesToAppend = InstructionRenderer.INSTRUCTION_BYTES_MIN_LENGTH - instruction.instructionBytes.length;
909
}
910
for (let i = 0; i < spacesToAppend; i++) {
911
sb.appendString(' ');
912
}
913
}
914
915
sb.appendString(instruction.instruction);
916
templateData.instruction.innerText = sb.build();
917
918
this.rerenderBackground(templateData.instruction, templateData.sourcecode, element);
919
}
920
921
disposeElement(element: IDisassembledInstructionEntry, index: number, templateData: IInstructionColumnTemplateData): void {
922
dispose(templateData.cellDisposable);
923
templateData.cellDisposable = [];
924
}
925
926
disposeTemplate(templateData: IInstructionColumnTemplateData): void {
927
dispose(templateData.disposables);
928
templateData.disposables = [];
929
}
930
931
private rerenderBackground(instruction: HTMLElement, sourceCode: HTMLElement, element?: IDisassembledInstructionEntry) {
932
if (element && this._disassemblyView.currentInstructionAddresses.includes(element.address)) {
933
instruction.style.background = this._topStackFrameColor?.toString() || 'transparent';
934
} else if (element?.address === this._disassemblyView.focusedInstructionAddress) {
935
instruction.style.background = this._focusedStackFrameColor?.toString() || 'transparent';
936
} else {
937
instruction.style.background = 'transparent';
938
}
939
}
940
941
private openSourceCode(instruction: DebugProtocol.DisassembledInstruction | undefined) {
942
if (instruction) {
943
const sourceURI = this.getUriFromSource(instruction);
944
const selection = instruction.endLine ? {
945
startLineNumber: instruction.line!,
946
endLineNumber: instruction.endLine,
947
startColumn: instruction.column || 1,
948
endColumn: instruction.endColumn || Constants.MAX_SAFE_SMALL_INTEGER,
949
} : {
950
startLineNumber: instruction.line!,
951
endLineNumber: instruction.line!,
952
startColumn: instruction.column || 1,
953
endColumn: instruction.endColumn || Constants.MAX_SAFE_SMALL_INTEGER,
954
};
955
956
this.editorService.openEditor({
957
resource: sourceURI,
958
description: localize('editorOpenedFromDisassemblyDescription', "from disassembly"),
959
options: {
960
preserveFocus: false,
961
selection: selection,
962
revealIfOpened: true,
963
selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport,
964
pinned: false,
965
}
966
});
967
}
968
}
969
970
private getUriFromSource(instruction: DebugProtocol.DisassembledInstruction): URI {
971
// Try to resolve path before consulting the debugSession.
972
const path = instruction.location!.path;
973
if (path && isUriString(path)) { // path looks like a uri
974
return this.uriService.asCanonicalUri(URI.parse(path));
975
}
976
// assume a filesystem path
977
if (path && isAbsolute(path)) {
978
return this.uriService.asCanonicalUri(URI.file(path));
979
}
980
981
return getUriFromSource(instruction.location!, instruction.location!.path, this._disassemblyView.debugSession!.getId(), this.uriService, this.logService);
982
}
983
984
private applyFontInfo(element: HTMLElement) {
985
applyFontInfo(element, this._disassemblyView.fontInfo);
986
element.style.whiteSpace = 'pre';
987
}
988
}
989
990
class AccessibilityProvider implements IListAccessibilityProvider<IDisassembledInstructionEntry> {
991
992
getWidgetAriaLabel(): string {
993
return localize('disassemblyView', "Disassembly View");
994
}
995
996
getAriaLabel(element: IDisassembledInstructionEntry): string | null {
997
let label = '';
998
999
const instruction = element.instruction;
1000
if (instruction.address !== '-1') {
1001
label += `${localize('instructionAddress', "Address")}: ${instruction.address}`;
1002
}
1003
if (instruction.instructionBytes) {
1004
label += `, ${localize('instructionBytes', "Bytes")}: ${instruction.instructionBytes}`;
1005
}
1006
label += `, ${localize(`instructionText`, "Instruction")}: ${instruction.instruction}`;
1007
1008
return label;
1009
}
1010
}
1011
1012
export class DisassemblyViewContribution implements IWorkbenchContribution {
1013
1014
private readonly _onDidActiveEditorChangeListener: IDisposable;
1015
private _onDidChangeModelLanguage: IDisposable | undefined;
1016
private _languageSupportsDisassembleRequest: IContextKey<boolean> | undefined;
1017
1018
constructor(
1019
@IEditorService editorService: IEditorService,
1020
@IDebugService debugService: IDebugService,
1021
@IContextKeyService contextKeyService: IContextKeyService
1022
) {
1023
contextKeyService.bufferChangeEvents(() => {
1024
this._languageSupportsDisassembleRequest = CONTEXT_LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST.bindTo(contextKeyService);
1025
});
1026
1027
const onDidActiveEditorChangeListener = () => {
1028
if (this._onDidChangeModelLanguage) {
1029
this._onDidChangeModelLanguage.dispose();
1030
this._onDidChangeModelLanguage = undefined;
1031
}
1032
1033
const activeTextEditorControl = editorService.activeTextEditorControl;
1034
if (isCodeEditor(activeTextEditorControl)) {
1035
const language = activeTextEditorControl.getModel()?.getLanguageId();
1036
// TODO: instead of using idDebuggerInterestedInLanguage, have a specific ext point for languages
1037
// support disassembly
1038
this._languageSupportsDisassembleRequest?.set(!!language && debugService.getAdapterManager().someDebuggerInterestedInLanguage(language));
1039
1040
this._onDidChangeModelLanguage = activeTextEditorControl.onDidChangeModelLanguage(e => {
1041
this._languageSupportsDisassembleRequest?.set(debugService.getAdapterManager().someDebuggerInterestedInLanguage(e.newLanguage));
1042
});
1043
} else {
1044
this._languageSupportsDisassembleRequest?.set(false);
1045
}
1046
};
1047
1048
onDidActiveEditorChangeListener();
1049
this._onDidActiveEditorChangeListener = editorService.onDidActiveEditorChange(onDidActiveEditorChangeListener);
1050
}
1051
1052
dispose(): void {
1053
this._onDidActiveEditorChangeListener.dispose();
1054
this._onDidChangeModelLanguage?.dispose();
1055
}
1056
}
1057
1058
CommandsRegistry.registerCommand({
1059
metadata: {
1060
description: COPY_ADDRESS_LABEL,
1061
},
1062
id: COPY_ADDRESS_ID,
1063
handler: async (accessor: ServicesAccessor, entry?: IDisassembledInstructionEntry) => {
1064
if (entry?.instruction?.address) {
1065
const clipboardService = accessor.get(IClipboardService);
1066
clipboardService.writeText(entry.instruction.address);
1067
}
1068
}
1069
});
1070
1071