blob: e476ae7b07dce129f1a7702375a42f708d295f37 [file] [log] [blame]
/* This testcase is part of GDB, the GNU debugger.
Copyright 2023 Free Software Foundation, Inc.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
#include <pthread.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
/* The delay that the main thread gives once all the worker threads have
reached the barrier before the main thread enters the function on which
GDB will have placed a breakpoint. */
#define MAIN_THREAD_DELAY 2
/* The maximum time we allow this test program to run for before an alarm
signal is sent and everything will exit. */
#define WATCHDOG_ALARM_TIME 600
/* Aliases for the signals used within this script. Each signal
corresponds to an action (from the FINAL_ACTION enum) that the signal
handler will perform. */
#define SPIN_SIGNAL SIGUSR1
#define SYSCALL_SIGNAL SIGUSR2
/* Describe the final action that a thread should perform. */
enum final_action
{
/* Thread should spin in an infinite loop. */
SPIN = 0,
/* Thread should block in a syscall. */
SYSCALL,
/* This is just a marker to allow for looping over the enum. */
LAST_ACTION
};
/* Where should the thread perform this action? */
enum exec_location
{
/* Just a normal thread, on a normal stack. */
NORMAL = 0,
/* In a signal handler, but use the normal stack. */
SIGNAL_HANDLER,
/* In a signal handler using an alternative stack. */
SIGNAL_ALT_STACK,
/* This is just a marker to allow for looping over the enum. */
LAST_LOCACTION
};
/* A descriptor for a single thread job. We create a new thread for each
job_description. */
struct job_description
{
/* What action should this thread perform. */
enum final_action action;
/* Where should the thread perform the action. */
enum exec_location location;
/* The actual thread handle, so we can join with the thread. */
pthread_t thread;
};
/* A pthread barrier, used to (try) and synchronise the threads. */
pthread_barrier_t global_barrier;
/* Return a list of jobs, and place the length of the list in *COUNT. */
struct job_description *
get_job_list (int *count)
{
/* The number of jobs. */
int num = LAST_ACTION * LAST_LOCACTION;
/* The uninitialised array of jobs. */
struct job_description *list
= malloc (num * sizeof (struct job_description));
assert (list != NULL);
/* Fill the array with all possible jobs. */
for (int i = 0; i < (int) LAST_ACTION; ++i)
for (int j = 0; j < (int) LAST_LOCACTION; ++j)
{
int idx = (i * LAST_LOCACTION) + j;
list[idx].action = (enum final_action) i;
list[idx].location = (enum exec_location) j;
}
/* Return the array of jobs. */
*count = num;
return list;
}
/* This function should never be called. If it is then an assertion will
trigger. */
void
assert_not_reached (void)
{
assert (0);
}
/* The function for a SPIN action. Just spins in a loop. The LOCATION
argument exists so GDB can identify the expected context for this
function. */
void
do_spin_task (enum exec_location location)
{
(void) location;
/* Let everyone know that we're about to perform our action. */
int res = pthread_barrier_wait (&global_barrier);
assert (res == PTHREAD_BARRIER_SERIAL_THREAD || res == 0);
while (1)
{
/* Nothing. */
}
}
/* The function for a SYSCALL action. Just spins in a loop. The LOCATION
argument exists so GDB can identify the expected context for this
function. */
void
do_syscall_task (enum exec_location location)
{
(void) location;
/* Let everyone know that we're about to perform our action. */
int res = pthread_barrier_wait (&global_barrier);
assert (res == PTHREAD_BARRIER_SERIAL_THREAD || res == 0);
sleep (600);
}
/* Return the required size for a sigaltstack. We start with a single
page, but do check against the system defined minimums. We don't run
much on the alternative stacks, so we don't need a huge one. */
size_t
get_stack_size (void)
{
size_t size = getpagesize (); /* Arbitrary starting size. */
if (size < SIGSTKSZ)
size = SIGSTKSZ;
if (size < MINSIGSTKSZ)
size = MINSIGSTKSZ;
return size;
}
/* A descriptor for an alternative stack. */
struct stack_descriptor
{
/* The base address of the alternative stack. This is the address that
must be freed to release the memory used by this stack. */
void *base;
/* The size of this alternative stack. Tracked just so we can query this
from GDB. */
size_t size;
};
/* Install an alternative signal stack. Return a descriptor for the newly
allocated alternative stack. */
struct stack_descriptor
setup_alt_stack (void)
{
size_t stack_size = get_stack_size ();
void *stack_area = malloc (stack_size);
stack_t stk;
stk.ss_sp = stack_area;
stk.ss_flags = 0;
stk.ss_size = stack_size;
int res = sigaltstack (&stk, NULL);
assert (res == 0);
struct stack_descriptor desc;
desc.base = stack_area;
desc.size = stack_size;
return desc;
}
/* Return true (non-zero) if we are currently on the alternative stack,
otherwise, return false (zero). */
int
on_alt_stack_p (void)
{
stack_t stk;
int res = sigaltstack (NULL, &stk);
assert (res == 0);
return (stk.ss_flags & SS_ONSTACK) != 0;
}
/* The signal handler function. All signals call here, so we use SIGNO
(the signal that was delivered) to decide what action to perform. This
function might, or might not, have been called on an alternative signal
stack. */
void
signal_handler (int signo)
{
enum exec_location location
= on_alt_stack_p () ? SIGNAL_ALT_STACK : SIGNAL_HANDLER;
switch (signo)
{
case SPIN_SIGNAL:
do_spin_task (location);
break;
case SYSCALL_SIGNAL:
do_syscall_task (location);
break;
default:
assert_not_reached ();
}
}
/* The thread worker function. ARG is a job_description pointer which
describes what this thread is expected to do. This function always
returns a NULL pointer. */
void *
thread_function (void *arg)
{
struct job_description *job = (struct job_description *) arg;
struct stack_descriptor desc = { NULL, 0 };
int sa_flags = 0;
switch (job->location)
{
case NORMAL:
/* This thread performs the worker action on the current thread,
select the correct worker function based on the requested
action. */
switch (job->action)
{
case SPIN:
do_spin_task (NORMAL);
break;
case SYSCALL:
do_syscall_task (NORMAL);
break;
default:
assert_not_reached ();
}
break;
case SIGNAL_ALT_STACK:
/* This thread is to perform its action in a signal handler on the
alternative stack. Install the alternative stack now, and then
fall through to the normal signal handler location code. */
desc = setup_alt_stack ();
assert (desc.base != NULL);
assert (desc.size > 0);
sa_flags = SA_ONSTACK;
/* Fall through. */
case SIGNAL_HANDLER:
{
/* This thread is to perform its action in a signal handler. We
might have just installed an alternative signal stack. */
int signo, res;
/* Select the correct signal number so that the signal handler will
perform the required action. */
switch (job->action)
{
case SPIN:
signo = SPIN_SIGNAL;
break;
case SYSCALL:
signo = SYSCALL_SIGNAL;
break;
default:
assert_not_reached ();
}
/* Now setup the signal handler. */
struct sigaction sa;
sa.sa_handler = signal_handler;
sigfillset (&sa.sa_mask);
sa.sa_flags = sa_flags;
res = sigaction (signo, &sa, NULL);
assert (res == 0);
/* Send the signal to this thread. */
res = pthread_kill (job->thread, signo);
assert (res == 0);
}
break;
default:
assert_not_reached ();
};
/* Free the alt-stack if we allocated one, if not DESC.BASE will be
NULL so this call is fine. */
free (desc.base);
/* Thread complete. */
return NULL;
}
void
start_job (struct job_description *job)
{
int res;
res = pthread_create (&job->thread, NULL, thread_function, job);
assert (res == 0);
}
/* Join with the thread for JOB. This will block until the thread for JOB
has finished. */
void
finalise_job (struct job_description *job)
{
int res;
void *retval;
res = pthread_join (job->thread, &retval);
assert (res == 0);
assert (retval == NULL);
}
/* Function that GDB can place a breakpoint on. */
void
breakpt (void)
{
/* Nothing. */
}
/* Function that triggers a crash, if the user has setup their environment
correctly this will dump a core file, which GDB can then examine. */
void
crash_function (void)
{
volatile int *p = 0;
volatile int n = *p;
(void) n;
}
/* Entry point. */
int
main ()
{
int job_count, res;
struct job_description *jobs = get_job_list (&job_count);
/* This test is going to park some threads inside infinite loops. Just
in case this program is left running, install an alarm that will cause
everything to exit. */
alarm (WATCHDOG_ALARM_TIME);
/* We want each worker thread (of which there are JOB_COUNT) plus the
main thread (hence + 1) to wait at the barrier. */
res = pthread_barrier_init (&global_barrier, NULL, job_count + 1);
assert (res == 0);
/* Start all the jobs. */
for (int i = 0; i < job_count; ++i)
start_job (&jobs[i]);
/* Notify all the worker threads that we're waiting for them. */
res = pthread_barrier_wait (&global_barrier);
assert (res == PTHREAD_BARRIER_SERIAL_THREAD || res == 0);
/* All we know at this point is that all the worker threads have reached
the barrier, which is just before they perform their action. But we
really want them to start their action.
There's really no way we can be 100% certain that the worker threads
have started their action, all we can do is wait for a short while and
hope that the machine we're running on is not too slow. */
sleep (MAIN_THREAD_DELAY);
/* A function that GDB can place a breakpoint on. By the time we get
here we are as sure as we can be that all of the worker threads have
started and are in their worker action (spinning, or syscall). */
breakpt ();
/* If GDB is not attached then this function will cause a crash, which
can be used to dump a core file, which GDB can then analyse. */
crash_function ();
/* Due to the crash we never expect to get here. Plus the worker actions
never terminate. But for completeness, here's where we join with all
the worker threads. */
for (int i = 0; i < job_count; ++i)
finalise_job (&jobs[i]);
/* Cleanup the barrier. */
res = pthread_barrier_destroy (&global_barrier);
assert (res == 0);
/* And clean up the jobs list. */
free (jobs);
return 0;
}