/* 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 . */ #include #include #include #include #include /* 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; }