Skip to content

Commit

Permalink
Add support for setting the cwd of the gdb process
Browse files Browse the repository at this point in the history
For some use cases the executable being debugged expects to find
its sources relative to the current working directory ($cwd)
instead of the compilation directory ($cdir).

This can happen in a couple of circumstances:

1. The source and executable have been moved from the location it was
compiled from.
2. The compiler did not save the compilation directory for the given file.

The compilation directory can be examined with "info source" in the
GDB debugger console.
  • Loading branch information
jonahgraham committed Oct 12, 2023
1 parent 3779028 commit 2b64973
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 14 deletions.
31 changes: 26 additions & 5 deletions src/GDBBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ import * as mi from './mi';
import { MIResponse } from './mi';
import { MIParser } from './MIParser';
import { VarManager } from './varManager';
import { compareVersions, getGdbVersion, createEnvValues } from './util';
import {
compareVersions,
getGdbVersion,
createEnvValues,
getGdbCwd,
} from './util';

export interface MIExecNextRequest {
reverse?: boolean;
Expand Down Expand Up @@ -73,15 +78,22 @@ export class GDBBackend extends events.EventEmitter {
requestArgs: LaunchRequestArguments | AttachRequestArguments
) {
const gdbPath = requestArgs.gdb || 'gdb';
this.gdbVersion = await getGdbVersion(gdbPath, requestArgs.environment);
this.gdbVersion = await getGdbVersion(
gdbPath,
getGdbCwd(requestArgs),
requestArgs.environment
);
let args = ['--interpreter=mi2'];
if (requestArgs.gdbArguments) {
args = args.concat(requestArgs.gdbArguments);
}
const gdbEnvironment = requestArgs.environment
? createEnvValues(process.env, requestArgs.environment)
: process.env;
this.proc = spawn(gdbPath, args, { env: gdbEnvironment });
this.proc = spawn(gdbPath, args, {
cwd: getGdbCwd(requestArgs),
env: gdbEnvironment,
});
if (this.proc.stdin == null || this.proc.stdout == null) {
throw new Error('Spawned GDB does not have stdout or stdin');
}
Expand All @@ -103,7 +115,11 @@ export class GDBBackend extends events.EventEmitter {
cb: (args: string[]) => Promise<void>
) {
const gdbPath = requestArgs.gdb || 'gdb';
this.gdbVersion = await getGdbVersion(gdbPath, requestArgs.environment);
this.gdbVersion = await getGdbVersion(
gdbPath,
getGdbCwd(requestArgs),
requestArgs.environment
);
// Use dynamic import to remove need for natively building this adapter
// Useful when 'spawnInClientTerminal' isn't needed, but adapter is distributed on multiple OS's
const { Pty } = await import('./native/pty');
Expand Down Expand Up @@ -201,9 +217,14 @@ export class GDBBackend extends events.EventEmitter {

public async supportsNewUi(
gdbPath?: string,
gdbCwd?: string,
environment?: Record<string, string | null>
): Promise<boolean> {
this.gdbVersion = await getGdbVersion(gdbPath || 'gdb', environment);
this.gdbVersion = await getGdbVersion(
gdbPath || 'gdb',
gdbCwd,
environment
);
return this.gdbVersionAtLeast('7.12');
}

Expand Down
14 changes: 10 additions & 4 deletions src/GDBDebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,18 @@ import {
} from './mi/data';
import { StoppedEvent } from './stoppedEvent';
import { VarObjType } from './varManager';
import { createEnvValues } from './util';
import { createEnvValues, getGdbCwd } from './util';

export interface RequestArguments extends DebugProtocol.LaunchRequestArguments {
gdb?: string;
gdbArguments?: string[];
gdbAsync?: boolean;
gdbNonStop?: boolean;
// defaults to the environment of the process of the adapter
environment?: Record<string, string | null>;
program: string;
cwd?: string; // TODO not implemented
// defaults to dirname of the program, if present or the cwd of the process of the adapter
cwd?: string;
verbose?: boolean;
logFile?: string;
openGdbConsole?: boolean;
Expand Down Expand Up @@ -442,7 +444,11 @@ export class GDBDebugSession extends LoggingDebugSession {
'cdt-gdb-adapter: openGdbConsole is not supported on this platform'
);
} else if (
!(await this.gdb.supportsNewUi(args.gdb, args.environment))
!(await this.gdb.supportsNewUi(
args.gdb,
getGdbCwd(args),
args.environment
))
) {
logger.warn(
`cdt-gdb-adapter: new-ui command not detected (${
Expand Down Expand Up @@ -478,7 +484,7 @@ export class GDBDebugSession extends LoggingDebugSession {
'runInTerminal',
{
kind: 'integrated',
cwd: process.cwd(),
cwd: getGdbCwd(requestArgs),
env: gdbEnvironment,
args: command,
} as DebugProtocol.RunInTerminalRequestArguments,
Expand Down
8 changes: 5 additions & 3 deletions src/GDBTargetDebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { DebugProtocol } from '@vscode/debugprotocol';
import { spawn, ChildProcess } from 'child_process';
import { SerialPort, ReadlineParser } from 'serialport';
import { Socket } from 'net';
import { createEnvValues } from './util';
import { createEnvValues, getGdbCwd } from './util';

interface UARTArguments {
// Path to the serial port connected to the UART on the board.
Expand Down Expand Up @@ -63,6 +63,7 @@ export interface TargetLaunchArguments extends TargetAttachArguments {
// defaults to 'gdbserver --once :0 ${args.program}' (requires gdbserver >= 7.3)
server?: string;
serverParameters?: string[];
// Specifies the working directory of gdbserver, defaults to environment in RequestArguments
environment?: Record<string, string | null>;
// Regular expression to extract port from by examinging stdout/err of server.
// Once server is launched, port will be set to this if port is not set.
Expand All @@ -74,7 +75,7 @@ export interface TargetLaunchArguments extends TargetAttachArguments {
serverStartupDelay?: number;
// Automatically kill the launched server when client issues a disconnect (default: true)
automaticallyKillServer?: boolean;
// Specifies the working directory of gdbserver
// Specifies the working directory of gdbserver, defaults to cwd in RequestArguments
cwd?: string;
}

Expand Down Expand Up @@ -209,7 +210,8 @@ export class GDBTargetDebugSession extends GDBDebugSession {
const target = args.target;
const serverExe =
target.server !== undefined ? target.server : 'gdbserver';
const serverCwd = target.cwd !== undefined ? target.cwd : args.cwd;
const serverCwd =
target.cwd !== undefined ? target.cwd : getGdbCwd(args);
const serverParams =
target.serverParameters !== undefined
? target.serverParameters
Expand Down
125 changes: 125 additions & 0 deletions src/integration-tests/gdbCwd.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*********************************************************************
* Copyright (c) 2023 Kichwa Coders Canada Inc. and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*********************************************************************/

import * as path from 'path';
import { CdtDebugClient } from './debugClient';
import {
fillDefaults,
resolveLineTagLocations,
standardBeforeEach,
testProgramsDir,
} from './utils';
import { expect } from 'chai';

/**
* To test that cwd is set properly we remove the compilation directory from the executable,
* see the makefile for that part, and then launch with a variety of executable/cwd locations
* to make sure that we can insert breakpoints when we expect to, and cannot insert breakpoints
* when we force gdb not to be able to find the source
*/
describe('gdb cwd', function () {
let dc: CdtDebugClient;
const program = path.join(testProgramsDir, 'cwd.exe');
const programRelocated = path.join(testProgramsDir, 'Debug', 'cwd.exe');
const src = path.join(testProgramsDir, 'cwd.c');
const lineTags = {
'STOP HERE': 0,
};

before(function () {
resolveLineTagLocations(src, lineTags);
});

beforeEach(async function () {
dc = await standardBeforeEach();
});

afterEach(async function () {
await dc.stop();
});

it('default cwd finds source in program directory', async function () {
await dc.launchRequest(
fillDefaults(this.test, {
program: program,
})
);

const bps = await dc.setBreakpointsRequest({
lines: [lineTags['STOP HERE']],
breakpoints: [{ line: lineTags['STOP HERE'], column: 1 }],
source: { path: src },
});
expect(bps.body.breakpoints[0].verified).to.eq(true);
});

it('explicit cwd finds source in program directory', async function () {
await dc.launchRequest(
fillDefaults(this.test, {
program: program,
cwd: testProgramsDir,
})
);

const bps = await dc.setBreakpointsRequest({
lines: [lineTags['STOP HERE']],
breakpoints: [{ line: lineTags['STOP HERE'], column: 1 }],
source: { path: src },
});
expect(bps.body.breakpoints[0].verified).to.eq(true);
});

it('default cwd does not find source with relocated program', async function () {
await dc.launchRequest(
fillDefaults(this.test, {
program: programRelocated,
})
);

const bps = await dc.setBreakpointsRequest({
lines: [lineTags['STOP HERE']],
breakpoints: [{ line: lineTags['STOP HERE'], column: 1 }],
source: { path: src },
});
expect(bps.body.breakpoints[0].verified).to.eq(false);
});

it('explicitly incorrect cwd does not finds source with relocated program', async function () {
await dc.launchRequest(
fillDefaults(this.test, {
program: programRelocated,
cwd: path.join(testProgramsDir, 'EmptyDir'),
})
);

const bps = await dc.setBreakpointsRequest({
lines: [lineTags['STOP HERE']],
breakpoints: [{ line: lineTags['STOP HERE'], column: 1 }],
source: { path: src },
});
expect(bps.body.breakpoints[0].verified).to.eq(false);
});

it('explicitly correct cwd does find source with relocated program', async function () {
await dc.launchRequest(
fillDefaults(this.test, {
program: programRelocated,
cwd: testProgramsDir,
})
);

const bps = await dc.setBreakpointsRequest({
lines: [lineTags['STOP HERE']],
breakpoints: [{ line: lineTags['STOP HERE'], column: 1 }],
source: { path: src },
});
expect(bps.body.breakpoints[0].verified).to.eq(true);
});
});
17 changes: 16 additions & 1 deletion src/integration-tests/test-programs/Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
BINS = empty empty\ space evaluate vars vars_cpp vars_env mem segv count disassemble functions loopforever MultiThread MultiThreadRunControl stderr bug275-测试
BINS = empty empty\ space evaluate vars vars_cpp vars_env mem segv count disassemble functions loopforever MultiThread MultiThreadRunControl stderr bug275-测试 cwd.exe

.PHONY: all
all: $(BINS)
Expand Down Expand Up @@ -26,6 +26,21 @@ count: count.o count_other.o count\ space.o
count\ space.o: count\ space.c
$(CC) -c "count space.c" -g3 -O0

# the cwd tests need to move around source and binary
# in ways that mean gdb cannot find the source automatically
# in the normal $cdir:$cwd and needs additional information
# to be provided
# debug-prefix-map used like this is to put an "incorrect"
# DW_AT_comp_dir in the debug info
cwd.o: cwd.c
$(CC) -fdebug-prefix-map=$(PWD)=. -c $< -g3 -O0

cwd.exe: cwd.o
$(LINK) -fdebug-prefix-map=$(PWD)=.
mkdir -p Debug
cp cwd.exe Debug
mkdir -p EmptyDir

empty: empty.o
$(LINK)

Expand Down
4 changes: 4 additions & 0 deletions src/integration-tests/test-programs/cwd.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
int main(int argc, char *argv[])
{
return 0; // STOP HERE
}
28 changes: 27 additions & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import { execFile } from 'child_process';
import { platform } from 'os';
import { promisify } from 'util';
import { dirname } from 'path';
import { existsSync } from 'fs';

/**
* This method actually launches 'gdb --version' to determine the version of
* the GDB that is being used.
Expand All @@ -19,6 +22,7 @@ import { promisify } from 'util';
*/
export async function getGdbVersion(
gdbPath: string,
gdbCwd?: string,
environment?: Record<string, string | null>
): Promise<string> {
const gdbEnvironment = environment
Expand All @@ -27,7 +31,7 @@ export async function getGdbVersion(
const { stdout, stderr } = await promisify(execFile)(
gdbPath,
['--version'],
{ env: gdbEnvironment }
{ cwd: gdbCwd, env: gdbEnvironment }
);

const gdbVersion = parseGdbVersionOutput(stdout);
Expand Down Expand Up @@ -154,3 +158,25 @@ export function createEnvValues(
}
return result;
}

/**
* Calculate the CWD that should be used to launch gdb based on the program
* being debugged or the explicitly set cwd in the launch arguments.
*
* Note that launchArgs.program is optional here in preparation for
* debugging where no program is specified. See #262
*
* @param launchArgs Launch Arguments to compute GDB cwd from
* @returns effective cwd to use
*/
export function getGdbCwd(launchArgs: {
program?: string;
cwd?: string;
}): string {
const cwd =
launchArgs.cwd ||
(launchArgs.program && existsSync(launchArgs.program)
? dirname(launchArgs.program)
: process.cwd());
return existsSync(cwd) ? cwd : process.cwd();
}

0 comments on commit 2b64973

Please sign in to comment.