mirror of
https://sourceware.org/git/binutils-gdb.git
synced 2024-11-30 13:33:53 +08:00
1fe75df7eb
Enhance 'thread apply' command to also accept [FLAG]... arguments. An example usage for this new argument: thread apply all -s frame apply all -s p some_local_var_somewhere Prints the thread id, frame location and some_local_var_somewhere value in frames of threads that have such local var. To make the life of the user easier, the most typical use cases have shortcuts : taas : shortcut for 'thread apply all -s' tfaas : shortcut for 'thread apply all -s frame apply all -s" An example usage : tfaas p some_local_var_somewhere same as the longer: 'thread apply all -s frame apply all -s p some_local_var_somewhere' gdb/ChangeLog 2018-07-12 Philippe Waroquiers <philippe.waroquiers@skynet.be> * thread.c (thr_try_catch_cmd): New function. (thread_apply_all_command): Handle qcs flags. (thread_apply_command): Handle qcs flags. (taas_command): New function. (tfaas_command): New function. (_initialize_thread): Update to setup the new commands 'taas and 'tfaas'. Change doc string for 'thread apply'.
2160 lines
51 KiB
C
2160 lines
51 KiB
C
/* Multi-process/thread control for GDB, the GNU debugger.
|
|
|
|
Copyright (C) 1986-2018 Free Software Foundation, Inc.
|
|
|
|
Contributed by Lynx Real-Time Systems, Inc. Los Gatos, CA.
|
|
|
|
This file is part of GDB.
|
|
|
|
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 "defs.h"
|
|
#include "symtab.h"
|
|
#include "frame.h"
|
|
#include "inferior.h"
|
|
#include "environ.h"
|
|
#include "value.h"
|
|
#include "target.h"
|
|
#include "gdbthread.h"
|
|
#include "command.h"
|
|
#include "gdbcmd.h"
|
|
#include "regcache.h"
|
|
#include "btrace.h"
|
|
|
|
#include <ctype.h>
|
|
#include <sys/types.h>
|
|
#include <signal.h>
|
|
#include "ui-out.h"
|
|
#include "observable.h"
|
|
#include "annotate.h"
|
|
#include "cli/cli-decode.h"
|
|
#include "gdb_regex.h"
|
|
#include "cli/cli-utils.h"
|
|
#include "thread-fsm.h"
|
|
#include "tid-parse.h"
|
|
#include <algorithm>
|
|
#include "common/gdb_optional.h"
|
|
|
|
/* Definition of struct thread_info exported to gdbthread.h. */
|
|
|
|
/* Prototypes for local functions. */
|
|
|
|
struct thread_info *thread_list = NULL;
|
|
static int highest_thread_num;
|
|
|
|
/* True if any thread is, or may be executing. We need to track this
|
|
separately because until we fully sync the thread list, we won't
|
|
know whether the target is fully stopped, even if we see stop
|
|
events for all known threads, because any of those threads may have
|
|
spawned new threads we haven't heard of yet. */
|
|
static int threads_executing;
|
|
|
|
static int thread_alive (struct thread_info *);
|
|
|
|
/* RAII type used to increase / decrease the refcount of each thread
|
|
in a given list of threads. */
|
|
|
|
class scoped_inc_dec_ref
|
|
{
|
|
public:
|
|
explicit scoped_inc_dec_ref (const std::vector<thread_info *> &thrds)
|
|
: m_thrds (thrds)
|
|
{
|
|
for (thread_info *thr : m_thrds)
|
|
thr->incref ();
|
|
}
|
|
|
|
~scoped_inc_dec_ref ()
|
|
{
|
|
for (thread_info *thr : m_thrds)
|
|
thr->decref ();
|
|
}
|
|
|
|
private:
|
|
const std::vector<thread_info *> &m_thrds;
|
|
};
|
|
|
|
|
|
struct thread_info*
|
|
inferior_thread (void)
|
|
{
|
|
struct thread_info *tp = find_thread_ptid (inferior_ptid);
|
|
gdb_assert (tp);
|
|
return tp;
|
|
}
|
|
|
|
/* Delete the breakpoint pointed at by BP_P, if there's one. */
|
|
|
|
static void
|
|
delete_thread_breakpoint (struct breakpoint **bp_p)
|
|
{
|
|
if (*bp_p != NULL)
|
|
{
|
|
delete_breakpoint (*bp_p);
|
|
*bp_p = NULL;
|
|
}
|
|
}
|
|
|
|
void
|
|
delete_step_resume_breakpoint (struct thread_info *tp)
|
|
{
|
|
if (tp != NULL)
|
|
delete_thread_breakpoint (&tp->control.step_resume_breakpoint);
|
|
}
|
|
|
|
void
|
|
delete_exception_resume_breakpoint (struct thread_info *tp)
|
|
{
|
|
if (tp != NULL)
|
|
delete_thread_breakpoint (&tp->control.exception_resume_breakpoint);
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
void
|
|
delete_single_step_breakpoints (struct thread_info *tp)
|
|
{
|
|
if (tp != NULL)
|
|
delete_thread_breakpoint (&tp->control.single_step_breakpoints);
|
|
}
|
|
|
|
/* Delete the breakpoint pointed at by BP_P at the next stop, if
|
|
there's one. */
|
|
|
|
static void
|
|
delete_at_next_stop (struct breakpoint **bp)
|
|
{
|
|
if (*bp != NULL)
|
|
{
|
|
(*bp)->disposition = disp_del_at_next_stop;
|
|
*bp = NULL;
|
|
}
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
int
|
|
thread_has_single_step_breakpoints_set (struct thread_info *tp)
|
|
{
|
|
return tp->control.single_step_breakpoints != NULL;
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
int
|
|
thread_has_single_step_breakpoint_here (struct thread_info *tp,
|
|
const address_space *aspace,
|
|
CORE_ADDR addr)
|
|
{
|
|
struct breakpoint *ss_bps = tp->control.single_step_breakpoints;
|
|
|
|
return (ss_bps != NULL
|
|
&& breakpoint_has_location_inserted_here (ss_bps, aspace, addr));
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
void
|
|
thread_cancel_execution_command (struct thread_info *thr)
|
|
{
|
|
if (thr->thread_fsm != NULL)
|
|
{
|
|
thread_fsm_clean_up (thr->thread_fsm, thr);
|
|
thread_fsm_delete (thr->thread_fsm);
|
|
thr->thread_fsm = NULL;
|
|
}
|
|
}
|
|
|
|
static void
|
|
clear_thread_inferior_resources (struct thread_info *tp)
|
|
{
|
|
/* NOTE: this will take care of any left-over step_resume breakpoints,
|
|
but not any user-specified thread-specific breakpoints. We can not
|
|
delete the breakpoint straight-off, because the inferior might not
|
|
be stopped at the moment. */
|
|
delete_at_next_stop (&tp->control.step_resume_breakpoint);
|
|
delete_at_next_stop (&tp->control.exception_resume_breakpoint);
|
|
delete_at_next_stop (&tp->control.single_step_breakpoints);
|
|
|
|
delete_longjmp_breakpoint_at_next_stop (tp->global_num);
|
|
|
|
bpstat_clear (&tp->control.stop_bpstat);
|
|
|
|
btrace_teardown (tp);
|
|
|
|
thread_cancel_execution_command (tp);
|
|
}
|
|
|
|
/* Set the TP's state as exited. */
|
|
|
|
static void
|
|
set_thread_exited (thread_info *tp, int silent)
|
|
{
|
|
/* Dead threads don't need to step-over. Remove from queue. */
|
|
if (tp->step_over_next != NULL)
|
|
thread_step_over_chain_remove (tp);
|
|
|
|
if (tp->state != THREAD_EXITED)
|
|
{
|
|
gdb::observers::thread_exit.notify (tp, silent);
|
|
|
|
/* Tag it as exited. */
|
|
tp->state = THREAD_EXITED;
|
|
|
|
/* Clear breakpoints, etc. associated with this thread. */
|
|
clear_thread_inferior_resources (tp);
|
|
}
|
|
}
|
|
|
|
void
|
|
init_thread_list (void)
|
|
{
|
|
struct thread_info *tp, *tmp;
|
|
|
|
highest_thread_num = 0;
|
|
|
|
ALL_THREADS_SAFE (tp, tmp)
|
|
{
|
|
if (tp->deletable ())
|
|
delete tp;
|
|
else
|
|
set_thread_exited (tp, 1);
|
|
}
|
|
|
|
thread_list = NULL;
|
|
threads_executing = 0;
|
|
}
|
|
|
|
/* Allocate a new thread of inferior INF with target id PTID and add
|
|
it to the thread list. */
|
|
|
|
static struct thread_info *
|
|
new_thread (struct inferior *inf, ptid_t ptid)
|
|
{
|
|
thread_info *tp = new thread_info (inf, ptid);
|
|
|
|
if (thread_list == NULL)
|
|
thread_list = tp;
|
|
else
|
|
{
|
|
struct thread_info *last;
|
|
|
|
for (last = thread_list; last->next != NULL; last = last->next)
|
|
;
|
|
last->next = tp;
|
|
}
|
|
|
|
return tp;
|
|
}
|
|
|
|
struct thread_info *
|
|
add_thread_silent (ptid_t ptid)
|
|
{
|
|
struct thread_info *tp;
|
|
struct inferior *inf = find_inferior_ptid (ptid);
|
|
gdb_assert (inf != NULL);
|
|
|
|
tp = find_thread_ptid (ptid);
|
|
if (tp)
|
|
/* Found an old thread with the same id. It has to be dead,
|
|
otherwise we wouldn't be adding a new thread with the same id.
|
|
The OS is reusing this id --- delete it, and recreate a new
|
|
one. */
|
|
{
|
|
/* In addition to deleting the thread, if this is the current
|
|
thread, then we need to take care that delete_thread doesn't
|
|
really delete the thread if it is inferior_ptid. Create a
|
|
new template thread in the list with an invalid ptid, switch
|
|
to it, delete the original thread, reset the new thread's
|
|
ptid, and switch to it. */
|
|
|
|
if (inferior_ptid == ptid)
|
|
{
|
|
thread_info *new_thr = new_thread (inf, null_ptid);
|
|
|
|
/* Make switch_to_thread not read from the thread. */
|
|
new_thr->state = THREAD_EXITED;
|
|
switch_to_no_thread ();
|
|
|
|
/* Now we can delete it. */
|
|
delete_thread (tp);
|
|
|
|
/* Now reset its ptid, and reswitch inferior_ptid to it. */
|
|
new_thr->ptid = ptid;
|
|
new_thr->state = THREAD_STOPPED;
|
|
switch_to_thread (new_thr);
|
|
|
|
gdb::observers::new_thread.notify (new_thr);
|
|
|
|
/* All done. */
|
|
return new_thr;
|
|
}
|
|
else
|
|
/* Just go ahead and delete it. */
|
|
delete_thread (tp);
|
|
}
|
|
|
|
tp = new_thread (inf, ptid);
|
|
gdb::observers::new_thread.notify (tp);
|
|
|
|
return tp;
|
|
}
|
|
|
|
struct thread_info *
|
|
add_thread_with_info (ptid_t ptid, private_thread_info *priv)
|
|
{
|
|
struct thread_info *result = add_thread_silent (ptid);
|
|
|
|
result->priv.reset (priv);
|
|
|
|
if (print_thread_events)
|
|
printf_unfiltered (_("[New %s]\n"), target_pid_to_str (ptid));
|
|
|
|
annotate_new_thread ();
|
|
return result;
|
|
}
|
|
|
|
struct thread_info *
|
|
add_thread (ptid_t ptid)
|
|
{
|
|
return add_thread_with_info (ptid, NULL);
|
|
}
|
|
|
|
private_thread_info::~private_thread_info () = default;
|
|
|
|
thread_info::thread_info (struct inferior *inf_, ptid_t ptid_)
|
|
: ptid (ptid_), inf (inf_)
|
|
{
|
|
gdb_assert (inf_ != NULL);
|
|
|
|
this->global_num = ++highest_thread_num;
|
|
this->per_inf_num = ++inf_->highest_thread_num;
|
|
|
|
/* Nothing to follow yet. */
|
|
memset (&this->pending_follow, 0, sizeof (this->pending_follow));
|
|
this->pending_follow.kind = TARGET_WAITKIND_SPURIOUS;
|
|
this->suspend.waitstatus.kind = TARGET_WAITKIND_IGNORE;
|
|
}
|
|
|
|
thread_info::~thread_info ()
|
|
{
|
|
xfree (this->name);
|
|
}
|
|
|
|
/* Add TP to the end of the step-over chain LIST_P. */
|
|
|
|
static void
|
|
step_over_chain_enqueue (struct thread_info **list_p, struct thread_info *tp)
|
|
{
|
|
gdb_assert (tp->step_over_next == NULL);
|
|
gdb_assert (tp->step_over_prev == NULL);
|
|
|
|
if (*list_p == NULL)
|
|
{
|
|
*list_p = tp;
|
|
tp->step_over_prev = tp->step_over_next = tp;
|
|
}
|
|
else
|
|
{
|
|
struct thread_info *head = *list_p;
|
|
struct thread_info *tail = head->step_over_prev;
|
|
|
|
tp->step_over_prev = tail;
|
|
tp->step_over_next = head;
|
|
head->step_over_prev = tp;
|
|
tail->step_over_next = tp;
|
|
}
|
|
}
|
|
|
|
/* Remove TP from step-over chain LIST_P. */
|
|
|
|
static void
|
|
step_over_chain_remove (struct thread_info **list_p, struct thread_info *tp)
|
|
{
|
|
gdb_assert (tp->step_over_next != NULL);
|
|
gdb_assert (tp->step_over_prev != NULL);
|
|
|
|
if (*list_p == tp)
|
|
{
|
|
if (tp == tp->step_over_next)
|
|
*list_p = NULL;
|
|
else
|
|
*list_p = tp->step_over_next;
|
|
}
|
|
|
|
tp->step_over_prev->step_over_next = tp->step_over_next;
|
|
tp->step_over_next->step_over_prev = tp->step_over_prev;
|
|
tp->step_over_prev = tp->step_over_next = NULL;
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
struct thread_info *
|
|
thread_step_over_chain_next (struct thread_info *tp)
|
|
{
|
|
struct thread_info *next = tp->step_over_next;
|
|
|
|
return (next == step_over_queue_head ? NULL : next);
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
int
|
|
thread_is_in_step_over_chain (struct thread_info *tp)
|
|
{
|
|
return (tp->step_over_next != NULL);
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
void
|
|
thread_step_over_chain_enqueue (struct thread_info *tp)
|
|
{
|
|
step_over_chain_enqueue (&step_over_queue_head, tp);
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
void
|
|
thread_step_over_chain_remove (struct thread_info *tp)
|
|
{
|
|
step_over_chain_remove (&step_over_queue_head, tp);
|
|
}
|
|
|
|
/* Delete thread TP. If SILENT, don't notify the observer of this
|
|
exit. */
|
|
|
|
static void
|
|
delete_thread_1 (thread_info *thr, bool silent)
|
|
{
|
|
struct thread_info *tp, *tpprev;
|
|
|
|
tpprev = NULL;
|
|
|
|
for (tp = thread_list; tp; tpprev = tp, tp = tp->next)
|
|
if (tp == thr)
|
|
break;
|
|
|
|
if (!tp)
|
|
return;
|
|
|
|
set_thread_exited (tp, silent);
|
|
|
|
if (!tp->deletable ())
|
|
{
|
|
/* Will be really deleted some other time. */
|
|
return;
|
|
}
|
|
|
|
if (tpprev)
|
|
tpprev->next = tp->next;
|
|
else
|
|
thread_list = tp->next;
|
|
|
|
delete tp;
|
|
}
|
|
|
|
/* Delete thread THREAD and notify of thread exit. If this is the
|
|
current thread, don't actually delete it, but tag it as exited and
|
|
do the notification. If this is the user selected thread, clear
|
|
it. */
|
|
|
|
void
|
|
delete_thread (thread_info *thread)
|
|
{
|
|
delete_thread_1 (thread, false /* not silent */);
|
|
}
|
|
|
|
void
|
|
delete_thread_silent (thread_info *thread)
|
|
{
|
|
delete_thread_1 (thread, true /* silent */);
|
|
}
|
|
|
|
struct thread_info *
|
|
find_thread_global_id (int global_id)
|
|
{
|
|
struct thread_info *tp;
|
|
|
|
for (tp = thread_list; tp; tp = tp->next)
|
|
if (tp->global_num == global_id)
|
|
return tp;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static struct thread_info *
|
|
find_thread_id (struct inferior *inf, int thr_num)
|
|
{
|
|
struct thread_info *tp;
|
|
|
|
for (tp = thread_list; tp; tp = tp->next)
|
|
if (tp->inf == inf && tp->per_inf_num == thr_num)
|
|
return tp;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Find a thread_info by matching PTID. */
|
|
|
|
struct thread_info *
|
|
find_thread_ptid (ptid_t ptid)
|
|
{
|
|
struct thread_info *tp;
|
|
|
|
for (tp = thread_list; tp; tp = tp->next)
|
|
if (tp->ptid == ptid)
|
|
return tp;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
struct thread_info *
|
|
find_thread_by_handle (struct value *thread_handle, struct inferior *inf)
|
|
{
|
|
return target_thread_handle_to_thread_info
|
|
(value_contents_all (thread_handle),
|
|
TYPE_LENGTH (value_type (thread_handle)),
|
|
inf);
|
|
}
|
|
|
|
/*
|
|
* Thread iterator function.
|
|
*
|
|
* Calls a callback function once for each thread, so long as
|
|
* the callback function returns false. If the callback function
|
|
* returns true, the iteration will end and the current thread
|
|
* will be returned. This can be useful for implementing a
|
|
* search for a thread with arbitrary attributes, or for applying
|
|
* some operation to every thread.
|
|
*
|
|
* FIXME: some of the existing functionality, such as
|
|
* "Thread apply all", might be rewritten using this functionality.
|
|
*/
|
|
|
|
struct thread_info *
|
|
iterate_over_threads (int (*callback) (struct thread_info *, void *),
|
|
void *data)
|
|
{
|
|
struct thread_info *tp, *next;
|
|
|
|
for (tp = thread_list; tp; tp = next)
|
|
{
|
|
next = tp->next;
|
|
if ((*callback) (tp, data))
|
|
return tp;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
int
|
|
thread_count (void)
|
|
{
|
|
int result = 0;
|
|
struct thread_info *tp;
|
|
|
|
for (tp = thread_list; tp; tp = tp->next)
|
|
++result;
|
|
|
|
return result;
|
|
}
|
|
|
|
/* Return the number of non-exited threads in the thread list. */
|
|
|
|
static int
|
|
live_threads_count (void)
|
|
{
|
|
int result = 0;
|
|
struct thread_info *tp;
|
|
|
|
ALL_NON_EXITED_THREADS (tp)
|
|
++result;
|
|
|
|
return result;
|
|
}
|
|
|
|
int
|
|
valid_global_thread_id (int global_id)
|
|
{
|
|
struct thread_info *tp;
|
|
|
|
for (tp = thread_list; tp; tp = tp->next)
|
|
if (tp->global_num == global_id)
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
in_thread_list (ptid_t ptid)
|
|
{
|
|
struct thread_info *tp;
|
|
|
|
for (tp = thread_list; tp; tp = tp->next)
|
|
if (tp->ptid == ptid)
|
|
return 1;
|
|
|
|
return 0; /* Never heard of 'im. */
|
|
}
|
|
|
|
/* Finds the first thread of the inferior. */
|
|
|
|
thread_info *
|
|
first_thread_of_inferior (inferior *inf)
|
|
{
|
|
struct thread_info *tp, *ret = NULL;
|
|
|
|
for (tp = thread_list; tp; tp = tp->next)
|
|
if (tp->inf == inf)
|
|
if (ret == NULL || tp->global_num < ret->global_num)
|
|
ret = tp;
|
|
|
|
return ret;
|
|
}
|
|
|
|
thread_info *
|
|
any_thread_of_inferior (inferior *inf)
|
|
{
|
|
struct thread_info *tp;
|
|
|
|
gdb_assert (inf->pid != 0);
|
|
|
|
/* Prefer the current thread. */
|
|
if (inf == current_inferior ())
|
|
return inferior_thread ();
|
|
|
|
ALL_NON_EXITED_THREADS (tp)
|
|
if (tp->inf == inf)
|
|
return tp;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
thread_info *
|
|
any_live_thread_of_inferior (inferior *inf)
|
|
{
|
|
struct thread_info *curr_tp = NULL;
|
|
struct thread_info *tp;
|
|
struct thread_info *tp_executing = NULL;
|
|
|
|
gdb_assert (inf != NULL && inf->pid != 0);
|
|
|
|
/* Prefer the current thread if it's not executing. */
|
|
if (inferior_ptid != null_ptid && current_inferior () == inf)
|
|
{
|
|
/* If the current thread is dead, forget it. If it's not
|
|
executing, use it. Otherwise, still choose it (below), but
|
|
only if no other non-executing thread is found. */
|
|
curr_tp = inferior_thread ();
|
|
if (curr_tp->state == THREAD_EXITED)
|
|
curr_tp = NULL;
|
|
else if (!curr_tp->executing)
|
|
return curr_tp;
|
|
}
|
|
|
|
ALL_NON_EXITED_THREADS (tp)
|
|
if (tp->inf == inf)
|
|
{
|
|
if (!tp->executing)
|
|
return tp;
|
|
|
|
tp_executing = tp;
|
|
}
|
|
|
|
/* If both the current thread and all live threads are executing,
|
|
prefer the current thread. */
|
|
if (curr_tp != NULL)
|
|
return curr_tp;
|
|
|
|
/* Otherwise, just return an executing thread, if any. */
|
|
return tp_executing;
|
|
}
|
|
|
|
/* Return true if TP is an active thread. */
|
|
static int
|
|
thread_alive (struct thread_info *tp)
|
|
{
|
|
if (tp->state == THREAD_EXITED)
|
|
return 0;
|
|
if (!target_thread_alive (tp->ptid))
|
|
return 0;
|
|
return 1;
|
|
}
|
|
|
|
/* See gdbthreads.h. */
|
|
|
|
void
|
|
prune_threads (void)
|
|
{
|
|
struct thread_info *tp, *tmp;
|
|
|
|
ALL_THREADS_SAFE (tp, tmp)
|
|
{
|
|
if (!thread_alive (tp))
|
|
delete_thread (tp);
|
|
}
|
|
}
|
|
|
|
/* See gdbthreads.h. */
|
|
|
|
void
|
|
delete_exited_threads (void)
|
|
{
|
|
struct thread_info *tp, *tmp;
|
|
|
|
ALL_THREADS_SAFE (tp, tmp)
|
|
{
|
|
if (tp->state == THREAD_EXITED)
|
|
delete_thread (tp);
|
|
}
|
|
}
|
|
|
|
/* Return true value if stack temporaies are enabled for the thread
|
|
TP. */
|
|
|
|
bool
|
|
thread_stack_temporaries_enabled_p (thread_info *tp)
|
|
{
|
|
if (tp == NULL)
|
|
return false;
|
|
else
|
|
return tp->stack_temporaries_enabled;
|
|
}
|
|
|
|
/* Push V on to the stack temporaries of the thread with id PTID. */
|
|
|
|
void
|
|
push_thread_stack_temporary (thread_info *tp, struct value *v)
|
|
{
|
|
gdb_assert (tp != NULL && tp->stack_temporaries_enabled);
|
|
tp->stack_temporaries.push_back (v);
|
|
}
|
|
|
|
/* Return true if VAL is among the stack temporaries of the thread
|
|
TP. Return false otherwise. */
|
|
|
|
bool
|
|
value_in_thread_stack_temporaries (struct value *val, thread_info *tp)
|
|
{
|
|
gdb_assert (tp != NULL && tp->stack_temporaries_enabled);
|
|
for (value *v : tp->stack_temporaries)
|
|
if (v == val)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/* Return the last of the stack temporaries for thread with id PTID.
|
|
Return NULL if there are no stack temporaries for the thread. */
|
|
|
|
value *
|
|
get_last_thread_stack_temporary (thread_info *tp)
|
|
{
|
|
struct value *lastval = NULL;
|
|
|
|
gdb_assert (tp != NULL);
|
|
if (!tp->stack_temporaries.empty ())
|
|
lastval = tp->stack_temporaries.back ();
|
|
|
|
return lastval;
|
|
}
|
|
|
|
void
|
|
thread_change_ptid (ptid_t old_ptid, ptid_t new_ptid)
|
|
{
|
|
struct inferior *inf;
|
|
struct thread_info *tp;
|
|
|
|
/* It can happen that what we knew as the target inferior id
|
|
changes. E.g, target remote may only discover the remote process
|
|
pid after adding the inferior to GDB's list. */
|
|
inf = find_inferior_ptid (old_ptid);
|
|
inf->pid = new_ptid.pid ();
|
|
|
|
tp = find_thread_ptid (old_ptid);
|
|
tp->ptid = new_ptid;
|
|
|
|
gdb::observers::thread_ptid_changed.notify (old_ptid, new_ptid);
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
void
|
|
set_resumed (ptid_t ptid, int resumed)
|
|
{
|
|
struct thread_info *tp;
|
|
int all = ptid == minus_one_ptid;
|
|
|
|
if (all || ptid.is_pid ())
|
|
{
|
|
for (tp = thread_list; tp; tp = tp->next)
|
|
if (all || tp->ptid.pid () == ptid.pid ())
|
|
tp->resumed = resumed;
|
|
}
|
|
else
|
|
{
|
|
tp = find_thread_ptid (ptid);
|
|
gdb_assert (tp != NULL);
|
|
tp->resumed = resumed;
|
|
}
|
|
}
|
|
|
|
/* Helper for set_running, that marks one thread either running or
|
|
stopped. */
|
|
|
|
static int
|
|
set_running_thread (struct thread_info *tp, int running)
|
|
{
|
|
int started = 0;
|
|
|
|
if (running && tp->state == THREAD_STOPPED)
|
|
started = 1;
|
|
tp->state = running ? THREAD_RUNNING : THREAD_STOPPED;
|
|
|
|
if (!running)
|
|
{
|
|
/* If the thread is now marked stopped, remove it from
|
|
the step-over queue, so that we don't try to resume
|
|
it until the user wants it to. */
|
|
if (tp->step_over_next != NULL)
|
|
thread_step_over_chain_remove (tp);
|
|
}
|
|
|
|
return started;
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
void
|
|
thread_info::set_running (bool running)
|
|
{
|
|
if (set_running_thread (this, running))
|
|
gdb::observers::target_resumed.notify (this->ptid);
|
|
}
|
|
|
|
void
|
|
set_running (ptid_t ptid, int running)
|
|
{
|
|
struct thread_info *tp;
|
|
int all = ptid == minus_one_ptid;
|
|
int any_started = 0;
|
|
|
|
/* We try not to notify the observer if no thread has actually changed
|
|
the running state -- merely to reduce the number of messages to
|
|
frontend. Frontend is supposed to handle multiple *running just fine. */
|
|
if (all || ptid.is_pid ())
|
|
{
|
|
for (tp = thread_list; tp; tp = tp->next)
|
|
if (all || tp->ptid.pid () == ptid.pid ())
|
|
{
|
|
if (tp->state == THREAD_EXITED)
|
|
continue;
|
|
|
|
if (set_running_thread (tp, running))
|
|
any_started = 1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
tp = find_thread_ptid (ptid);
|
|
gdb_assert (tp != NULL);
|
|
gdb_assert (tp->state != THREAD_EXITED);
|
|
if (set_running_thread (tp, running))
|
|
any_started = 1;
|
|
}
|
|
if (any_started)
|
|
gdb::observers::target_resumed.notify (ptid);
|
|
}
|
|
|
|
static int
|
|
is_thread_state (ptid_t ptid, enum thread_state state)
|
|
{
|
|
struct thread_info *tp;
|
|
|
|
tp = find_thread_ptid (ptid);
|
|
gdb_assert (tp);
|
|
return tp->state == state;
|
|
}
|
|
|
|
int
|
|
is_stopped (ptid_t ptid)
|
|
{
|
|
return is_thread_state (ptid, THREAD_STOPPED);
|
|
}
|
|
|
|
int
|
|
is_exited (ptid_t ptid)
|
|
{
|
|
return is_thread_state (ptid, THREAD_EXITED);
|
|
}
|
|
|
|
int
|
|
is_running (ptid_t ptid)
|
|
{
|
|
return is_thread_state (ptid, THREAD_RUNNING);
|
|
}
|
|
|
|
int
|
|
is_executing (ptid_t ptid)
|
|
{
|
|
struct thread_info *tp;
|
|
|
|
tp = find_thread_ptid (ptid);
|
|
gdb_assert (tp);
|
|
return tp->executing;
|
|
}
|
|
|
|
/* Helper for set_executing. Set's the thread's 'executing' field
|
|
from EXECUTING, and if EXECUTING is true also clears the thread's
|
|
stop_pc. */
|
|
|
|
static void
|
|
set_executing_thread (thread_info *thr, bool executing)
|
|
{
|
|
thr->executing = executing;
|
|
if (executing)
|
|
thr->suspend.stop_pc = ~(CORE_ADDR) 0;
|
|
}
|
|
|
|
void
|
|
set_executing (ptid_t ptid, int executing)
|
|
{
|
|
struct thread_info *tp;
|
|
int all = ptid == minus_one_ptid;
|
|
|
|
if (all || ptid.is_pid ())
|
|
{
|
|
for (tp = thread_list; tp; tp = tp->next)
|
|
if (all || tp->ptid.pid () == ptid.pid ())
|
|
set_executing_thread (tp, executing);
|
|
}
|
|
else
|
|
{
|
|
tp = find_thread_ptid (ptid);
|
|
gdb_assert (tp);
|
|
set_executing_thread (tp, executing);
|
|
}
|
|
|
|
/* It only takes one running thread to spawn more threads.*/
|
|
if (executing)
|
|
threads_executing = 1;
|
|
/* Only clear the flag if the caller is telling us everything is
|
|
stopped. */
|
|
else if (minus_one_ptid == ptid)
|
|
threads_executing = 0;
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
int
|
|
threads_are_executing (void)
|
|
{
|
|
return threads_executing;
|
|
}
|
|
|
|
void
|
|
set_stop_requested (ptid_t ptid, int stop)
|
|
{
|
|
struct thread_info *tp;
|
|
int all = ptid == minus_one_ptid;
|
|
|
|
if (all || ptid.is_pid ())
|
|
{
|
|
for (tp = thread_list; tp; tp = tp->next)
|
|
if (all || tp->ptid.pid () == ptid.pid ())
|
|
tp->stop_requested = stop;
|
|
}
|
|
else
|
|
{
|
|
tp = find_thread_ptid (ptid);
|
|
gdb_assert (tp);
|
|
tp->stop_requested = stop;
|
|
}
|
|
|
|
/* Call the stop requested observer so other components of GDB can
|
|
react to this request. */
|
|
if (stop)
|
|
gdb::observers::thread_stop_requested.notify (ptid);
|
|
}
|
|
|
|
void
|
|
finish_thread_state (ptid_t ptid)
|
|
{
|
|
struct thread_info *tp;
|
|
int all;
|
|
int any_started = 0;
|
|
|
|
all = ptid == minus_one_ptid;
|
|
|
|
if (all || ptid.is_pid ())
|
|
{
|
|
for (tp = thread_list; tp; tp = tp->next)
|
|
{
|
|
if (tp->state == THREAD_EXITED)
|
|
continue;
|
|
if (all || ptid.pid () == tp->ptid.pid ())
|
|
{
|
|
if (set_running_thread (tp, tp->executing))
|
|
any_started = 1;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
tp = find_thread_ptid (ptid);
|
|
gdb_assert (tp);
|
|
if (tp->state != THREAD_EXITED)
|
|
{
|
|
if (set_running_thread (tp, tp->executing))
|
|
any_started = 1;
|
|
}
|
|
}
|
|
|
|
if (any_started)
|
|
gdb::observers::target_resumed.notify (ptid);
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
void
|
|
validate_registers_access (void)
|
|
{
|
|
/* No selected thread, no registers. */
|
|
if (inferior_ptid == null_ptid)
|
|
error (_("No thread selected."));
|
|
|
|
thread_info *tp = inferior_thread ();
|
|
|
|
/* Don't try to read from a dead thread. */
|
|
if (tp->state == THREAD_EXITED)
|
|
error (_("The current thread has terminated"));
|
|
|
|
/* ... or from a spinning thread. FIXME: This isn't actually fully
|
|
correct. It'll allow an user-requested access (e.g., "print $pc"
|
|
at the prompt) when a thread is not executing for some internal
|
|
reason, but is marked running from the user's perspective. E.g.,
|
|
the thread is waiting for its turn in the step-over queue. */
|
|
if (tp->executing)
|
|
error (_("Selected thread is running."));
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
bool
|
|
can_access_registers_thread (thread_info *thread)
|
|
{
|
|
/* No thread, no registers. */
|
|
if (thread == NULL)
|
|
return false;
|
|
|
|
/* Don't try to read from a dead thread. */
|
|
if (thread->state == THREAD_EXITED)
|
|
return false;
|
|
|
|
/* ... or from a spinning thread. FIXME: see validate_registers_access. */
|
|
if (thread->executing)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
int
|
|
pc_in_thread_step_range (CORE_ADDR pc, struct thread_info *thread)
|
|
{
|
|
return (pc >= thread->control.step_range_start
|
|
&& pc < thread->control.step_range_end);
|
|
}
|
|
|
|
/* Helper for print_thread_info. Returns true if THR should be
|
|
printed. If REQUESTED_THREADS, a list of GDB ids/ranges, is not
|
|
NULL, only print THR if its ID is included in the list. GLOBAL_IDS
|
|
is true if REQUESTED_THREADS is list of global IDs, false if a list
|
|
of per-inferior thread ids. If PID is not -1, only print THR if it
|
|
is a thread from the process PID. Otherwise, threads from all
|
|
attached PIDs are printed. If both REQUESTED_THREADS is not NULL
|
|
and PID is not -1, then the thread is printed if it belongs to the
|
|
specified process. Otherwise, an error is raised. */
|
|
|
|
static int
|
|
should_print_thread (const char *requested_threads, int default_inf_num,
|
|
int global_ids, int pid, struct thread_info *thr)
|
|
{
|
|
if (requested_threads != NULL && *requested_threads != '\0')
|
|
{
|
|
int in_list;
|
|
|
|
if (global_ids)
|
|
in_list = number_is_in_list (requested_threads, thr->global_num);
|
|
else
|
|
in_list = tid_is_in_list (requested_threads, default_inf_num,
|
|
thr->inf->num, thr->per_inf_num);
|
|
if (!in_list)
|
|
return 0;
|
|
}
|
|
|
|
if (pid != -1 && thr->ptid.pid () != pid)
|
|
{
|
|
if (requested_threads != NULL && *requested_threads != '\0')
|
|
error (_("Requested thread not found in requested process"));
|
|
return 0;
|
|
}
|
|
|
|
if (thr->state == THREAD_EXITED)
|
|
return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* Return the string to display in "info threads"'s "Target Id"
|
|
column, for TP. */
|
|
|
|
static std::string
|
|
thread_target_id_str (thread_info *tp)
|
|
{
|
|
const char *target_id = target_pid_to_str (tp->ptid);
|
|
const char *extra_info = target_extra_thread_info (tp);
|
|
const char *name = tp->name != nullptr ? tp->name : target_thread_name (tp);
|
|
|
|
if (extra_info != nullptr && name != nullptr)
|
|
return string_printf ("%s \"%s\" (%s)", target_id, name, extra_info);
|
|
else if (extra_info != nullptr)
|
|
return string_printf ("%s (%s)", target_id, extra_info);
|
|
else if (name != nullptr)
|
|
return string_printf ("%s \"%s\"", target_id, name);
|
|
else
|
|
return target_id;
|
|
}
|
|
|
|
/* Like print_thread_info, but in addition, GLOBAL_IDS indicates
|
|
whether REQUESTED_THREADS is a list of global or per-inferior
|
|
thread ids. */
|
|
|
|
static void
|
|
print_thread_info_1 (struct ui_out *uiout, const char *requested_threads,
|
|
int global_ids, int pid,
|
|
int show_global_ids)
|
|
{
|
|
struct thread_info *tp;
|
|
struct inferior *inf;
|
|
int default_inf_num = current_inferior ()->num;
|
|
|
|
update_thread_list ();
|
|
|
|
/* Whether we saw any thread. */
|
|
bool any_thread = false;
|
|
/* Whether the current thread is exited. */
|
|
bool current_exited = false;
|
|
|
|
thread_info *current_thread = (inferior_ptid != null_ptid
|
|
? inferior_thread () : NULL);
|
|
|
|
{
|
|
/* For backward compatibility, we make a list for MI. A table is
|
|
preferable for the CLI, though, because it shows table
|
|
headers. */
|
|
gdb::optional<ui_out_emit_list> list_emitter;
|
|
gdb::optional<ui_out_emit_table> table_emitter;
|
|
|
|
if (uiout->is_mi_like_p ())
|
|
list_emitter.emplace (uiout, "threads");
|
|
else
|
|
{
|
|
int n_threads = 0;
|
|
/* The width of the "Target Id" column. Grown below to
|
|
accommodate the largest entry. */
|
|
size_t target_id_col_width = 17;
|
|
|
|
ALL_THREADS (tp)
|
|
{
|
|
if (!should_print_thread (requested_threads, default_inf_num,
|
|
global_ids, pid, tp))
|
|
continue;
|
|
|
|
if (!uiout->is_mi_like_p ())
|
|
{
|
|
target_id_col_width
|
|
= std::max (target_id_col_width,
|
|
thread_target_id_str (tp).size ());
|
|
}
|
|
|
|
++n_threads;
|
|
}
|
|
|
|
if (n_threads == 0)
|
|
{
|
|
if (requested_threads == NULL || *requested_threads == '\0')
|
|
uiout->message (_("No threads.\n"));
|
|
else
|
|
uiout->message (_("No threads match '%s'.\n"),
|
|
requested_threads);
|
|
return;
|
|
}
|
|
|
|
table_emitter.emplace (uiout, show_global_ids ? 5 : 4,
|
|
n_threads, "threads");
|
|
|
|
uiout->table_header (1, ui_left, "current", "");
|
|
uiout->table_header (4, ui_left, "id-in-tg", "Id");
|
|
if (show_global_ids)
|
|
uiout->table_header (4, ui_left, "id", "GId");
|
|
uiout->table_header (target_id_col_width, ui_left,
|
|
"target-id", "Target Id");
|
|
uiout->table_header (1, ui_left, "frame", "Frame");
|
|
uiout->table_body ();
|
|
}
|
|
|
|
/* We'll be switching threads temporarily. */
|
|
scoped_restore_current_thread restore_thread;
|
|
|
|
ALL_THREADS_BY_INFERIOR (inf, tp)
|
|
{
|
|
int core;
|
|
|
|
any_thread = true;
|
|
if (tp == current_thread && tp->state == THREAD_EXITED)
|
|
current_exited = true;
|
|
|
|
if (!should_print_thread (requested_threads, default_inf_num,
|
|
global_ids, pid, tp))
|
|
continue;
|
|
|
|
ui_out_emit_tuple tuple_emitter (uiout, NULL);
|
|
|
|
if (!uiout->is_mi_like_p ())
|
|
{
|
|
if (tp == current_thread)
|
|
uiout->field_string ("current", "*");
|
|
else
|
|
uiout->field_skip ("current");
|
|
|
|
uiout->field_string ("id-in-tg", print_thread_id (tp));
|
|
}
|
|
|
|
if (show_global_ids || uiout->is_mi_like_p ())
|
|
uiout->field_int ("id", tp->global_num);
|
|
|
|
/* For the CLI, we stuff everything into the target-id field.
|
|
This is a gross hack to make the output come out looking
|
|
correct. The underlying problem here is that ui-out has no
|
|
way to specify that a field's space allocation should be
|
|
shared by several fields. For MI, we do the right thing
|
|
instead. */
|
|
|
|
if (uiout->is_mi_like_p ())
|
|
{
|
|
uiout->field_string ("target-id", target_pid_to_str (tp->ptid));
|
|
|
|
const char *extra_info = target_extra_thread_info (tp);
|
|
if (extra_info != nullptr)
|
|
uiout->field_string ("details", extra_info);
|
|
|
|
const char *name = (tp->name != nullptr
|
|
? tp->name
|
|
: target_thread_name (tp));
|
|
if (name != NULL)
|
|
uiout->field_string ("name", name);
|
|
}
|
|
else
|
|
{
|
|
uiout->field_string ("target-id",
|
|
thread_target_id_str (tp).c_str ());
|
|
}
|
|
|
|
if (tp->state == THREAD_RUNNING)
|
|
uiout->text ("(running)\n");
|
|
else
|
|
{
|
|
/* The switch below puts us at the top of the stack (leaf
|
|
frame). */
|
|
switch_to_thread (tp);
|
|
print_stack_frame (get_selected_frame (NULL),
|
|
/* For MI output, print frame level. */
|
|
uiout->is_mi_like_p (),
|
|
LOCATION, 0);
|
|
}
|
|
|
|
if (uiout->is_mi_like_p ())
|
|
{
|
|
const char *state = "stopped";
|
|
|
|
if (tp->state == THREAD_RUNNING)
|
|
state = "running";
|
|
uiout->field_string ("state", state);
|
|
}
|
|
|
|
core = target_core_of_thread (tp->ptid);
|
|
if (uiout->is_mi_like_p () && core != -1)
|
|
uiout->field_int ("core", core);
|
|
}
|
|
|
|
/* This end scope restores the current thread and the frame
|
|
selected before the "info threads" command, and it finishes the
|
|
ui-out list or table. */
|
|
}
|
|
|
|
if (pid == -1 && requested_threads == NULL)
|
|
{
|
|
if (uiout->is_mi_like_p () && inferior_ptid != null_ptid)
|
|
uiout->field_int ("current-thread-id", current_thread->global_num);
|
|
|
|
if (inferior_ptid != null_ptid && current_exited)
|
|
uiout->message ("\n\
|
|
The current thread <Thread ID %s> has terminated. See `help thread'.\n",
|
|
print_thread_id (inferior_thread ()));
|
|
else if (any_thread && inferior_ptid == null_ptid)
|
|
uiout->message ("\n\
|
|
No selected thread. See `help thread'.\n");
|
|
}
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
void
|
|
print_thread_info (struct ui_out *uiout, char *requested_threads, int pid)
|
|
{
|
|
print_thread_info_1 (uiout, requested_threads, 1, pid, 0);
|
|
}
|
|
|
|
/* Implementation of the "info threads" command.
|
|
|
|
Note: this has the drawback that it _really_ switches
|
|
threads, which frees the frame cache. A no-side
|
|
effects info-threads command would be nicer. */
|
|
|
|
static void
|
|
info_threads_command (const char *arg, int from_tty)
|
|
{
|
|
int show_global_ids = 0;
|
|
|
|
if (arg != NULL
|
|
&& check_for_argument (&arg, "-gid", sizeof ("-gid") - 1))
|
|
{
|
|
arg = skip_spaces (arg);
|
|
show_global_ids = 1;
|
|
}
|
|
|
|
print_thread_info_1 (current_uiout, arg, 0, -1, show_global_ids);
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
void
|
|
switch_to_thread_no_regs (struct thread_info *thread)
|
|
{
|
|
struct inferior *inf = thread->inf;
|
|
|
|
set_current_program_space (inf->pspace);
|
|
set_current_inferior (inf);
|
|
|
|
inferior_ptid = thread->ptid;
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
void
|
|
switch_to_no_thread ()
|
|
{
|
|
if (inferior_ptid == null_ptid)
|
|
return;
|
|
|
|
inferior_ptid = null_ptid;
|
|
reinit_frame_cache ();
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
void
|
|
switch_to_thread (thread_info *thr)
|
|
{
|
|
gdb_assert (thr != NULL);
|
|
|
|
if (inferior_ptid == thr->ptid)
|
|
return;
|
|
|
|
switch_to_thread_no_regs (thr);
|
|
|
|
reinit_frame_cache ();
|
|
}
|
|
|
|
/* See common/common-gdbthread.h. */
|
|
|
|
void
|
|
switch_to_thread (ptid_t ptid)
|
|
{
|
|
thread_info *thr = find_thread_ptid (ptid);
|
|
switch_to_thread (thr);
|
|
}
|
|
|
|
static void
|
|
restore_selected_frame (struct frame_id a_frame_id, int frame_level)
|
|
{
|
|
struct frame_info *frame = NULL;
|
|
int count;
|
|
|
|
/* This means there was no selected frame. */
|
|
if (frame_level == -1)
|
|
{
|
|
select_frame (NULL);
|
|
return;
|
|
}
|
|
|
|
gdb_assert (frame_level >= 0);
|
|
|
|
/* Restore by level first, check if the frame id is the same as
|
|
expected. If that fails, try restoring by frame id. If that
|
|
fails, nothing to do, just warn the user. */
|
|
|
|
count = frame_level;
|
|
frame = find_relative_frame (get_current_frame (), &count);
|
|
if (count == 0
|
|
&& frame != NULL
|
|
/* The frame ids must match - either both valid or both outer_frame_id.
|
|
The latter case is not failsafe, but since it's highly unlikely
|
|
the search by level finds the wrong frame, it's 99.9(9)% of
|
|
the time (for all practical purposes) safe. */
|
|
&& frame_id_eq (get_frame_id (frame), a_frame_id))
|
|
{
|
|
/* Cool, all is fine. */
|
|
select_frame (frame);
|
|
return;
|
|
}
|
|
|
|
frame = frame_find_by_id (a_frame_id);
|
|
if (frame != NULL)
|
|
{
|
|
/* Cool, refound it. */
|
|
select_frame (frame);
|
|
return;
|
|
}
|
|
|
|
/* Nothing else to do, the frame layout really changed. Select the
|
|
innermost stack frame. */
|
|
select_frame (get_current_frame ());
|
|
|
|
/* Warn the user. */
|
|
if (frame_level > 0 && !current_uiout->is_mi_like_p ())
|
|
{
|
|
warning (_("Couldn't restore frame #%d in "
|
|
"current thread. Bottom (innermost) frame selected:"),
|
|
frame_level);
|
|
/* For MI, we should probably have a notification about
|
|
current frame change. But this error is not very
|
|
likely, so don't bother for now. */
|
|
print_stack_frame (get_selected_frame (NULL), 1, SRC_AND_LOC, 1);
|
|
}
|
|
}
|
|
|
|
scoped_restore_current_thread::~scoped_restore_current_thread ()
|
|
{
|
|
/* If an entry of thread_info was previously selected, it won't be
|
|
deleted because we've increased its refcount. The thread represented
|
|
by this thread_info entry may have already exited (due to normal exit,
|
|
detach, etc), so the thread_info.state is THREAD_EXITED. */
|
|
if (m_thread != NULL
|
|
/* If the previously selected thread belonged to a process that has
|
|
in the mean time exited (or killed, detached, etc.), then don't revert
|
|
back to it, but instead simply drop back to no thread selected. */
|
|
&& m_inf->pid != 0)
|
|
switch_to_thread (m_thread);
|
|
else
|
|
{
|
|
switch_to_no_thread ();
|
|
set_current_inferior (m_inf);
|
|
}
|
|
|
|
/* The running state of the originally selected thread may have
|
|
changed, so we have to recheck it here. */
|
|
if (inferior_ptid != null_ptid
|
|
&& m_was_stopped
|
|
&& m_thread->state == THREAD_STOPPED
|
|
&& target_has_registers
|
|
&& target_has_stack
|
|
&& target_has_memory)
|
|
restore_selected_frame (m_selected_frame_id, m_selected_frame_level);
|
|
|
|
if (m_thread != NULL)
|
|
m_thread->decref ();
|
|
m_inf->decref ();
|
|
}
|
|
|
|
scoped_restore_current_thread::scoped_restore_current_thread ()
|
|
{
|
|
m_thread = NULL;
|
|
m_inf = current_inferior ();
|
|
|
|
if (inferior_ptid != null_ptid)
|
|
{
|
|
thread_info *tp = inferior_thread ();
|
|
struct frame_info *frame;
|
|
|
|
m_was_stopped = tp->state == THREAD_STOPPED;
|
|
if (m_was_stopped
|
|
&& target_has_registers
|
|
&& target_has_stack
|
|
&& target_has_memory)
|
|
{
|
|
/* When processing internal events, there might not be a
|
|
selected frame. If we naively call get_selected_frame
|
|
here, then we can end up reading debuginfo for the
|
|
current frame, but we don't generally need the debuginfo
|
|
at this point. */
|
|
frame = get_selected_frame_if_set ();
|
|
}
|
|
else
|
|
frame = NULL;
|
|
|
|
m_selected_frame_id = get_frame_id (frame);
|
|
m_selected_frame_level = frame_relative_level (frame);
|
|
|
|
tp->incref ();
|
|
m_thread = tp;
|
|
}
|
|
|
|
m_inf->incref ();
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
int
|
|
show_thread_that_caused_stop (void)
|
|
{
|
|
return highest_thread_num > 1;
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
int
|
|
show_inferior_qualified_tids (void)
|
|
{
|
|
return (inferior_list->next != NULL || inferior_list->num != 1);
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
const char *
|
|
print_thread_id (struct thread_info *thr)
|
|
{
|
|
char *s = get_print_cell ();
|
|
|
|
if (show_inferior_qualified_tids ())
|
|
xsnprintf (s, PRINT_CELL_SIZE, "%d.%d", thr->inf->num, thr->per_inf_num);
|
|
else
|
|
xsnprintf (s, PRINT_CELL_SIZE, "%d", thr->per_inf_num);
|
|
return s;
|
|
}
|
|
|
|
/* If true, tp_array_compar should sort in ascending order, otherwise
|
|
in descending order. */
|
|
|
|
static bool tp_array_compar_ascending;
|
|
|
|
/* Sort an array for struct thread_info pointers by thread ID (first
|
|
by inferior number, and then by per-inferior thread number). The
|
|
order is determined by TP_ARRAY_COMPAR_ASCENDING. */
|
|
|
|
static bool
|
|
tp_array_compar (const thread_info *a, const thread_info *b)
|
|
{
|
|
if (a->inf->num != b->inf->num)
|
|
{
|
|
if (tp_array_compar_ascending)
|
|
return a->inf->num < b->inf->num;
|
|
else
|
|
return a->inf->num > b->inf->num;
|
|
}
|
|
|
|
if (tp_array_compar_ascending)
|
|
return (a->per_inf_num < b->per_inf_num);
|
|
else
|
|
return (a->per_inf_num > b->per_inf_num);
|
|
}
|
|
|
|
/* Switch to thread THR and execute CMD.
|
|
FLAGS.QUIET controls the printing of the thread information.
|
|
FLAGS.CONT and FLAGS.SILENT control how to handle errors. */
|
|
|
|
static void
|
|
thr_try_catch_cmd (thread_info *thr, const char *cmd, int from_tty,
|
|
const qcs_flags &flags)
|
|
{
|
|
switch_to_thread (thr);
|
|
TRY
|
|
{
|
|
std::string cmd_result = execute_command_to_string (cmd, from_tty);
|
|
if (!flags.silent || cmd_result.length () > 0)
|
|
{
|
|
if (!flags.quiet)
|
|
printf_filtered (_("\nThread %s (%s):\n"),
|
|
print_thread_id (thr),
|
|
target_pid_to_str (inferior_ptid));
|
|
printf_filtered ("%s", cmd_result.c_str ());
|
|
}
|
|
}
|
|
CATCH (ex, RETURN_MASK_ERROR)
|
|
{
|
|
if (!flags.silent)
|
|
{
|
|
if (!flags.quiet)
|
|
printf_filtered (_("\nThread %s (%s):\n"),
|
|
print_thread_id (thr),
|
|
target_pid_to_str (inferior_ptid));
|
|
if (flags.cont)
|
|
printf_filtered ("%s\n", ex.message);
|
|
else
|
|
throw_exception (ex);
|
|
}
|
|
}
|
|
END_CATCH;
|
|
}
|
|
|
|
/* Apply a GDB command to a list of threads. List syntax is a whitespace
|
|
separated list of numbers, or ranges, or the keyword `all'. Ranges consist
|
|
of two numbers separated by a hyphen. Examples:
|
|
|
|
thread apply 1 2 7 4 backtrace Apply backtrace cmd to threads 1,2,7,4
|
|
thread apply 2-7 9 p foo(1) Apply p foo(1) cmd to threads 2->7 & 9
|
|
thread apply all x/i $pc Apply x/i $pc cmd to all threads. */
|
|
|
|
static void
|
|
thread_apply_all_command (const char *cmd, int from_tty)
|
|
{
|
|
qcs_flags flags;
|
|
|
|
tp_array_compar_ascending = false;
|
|
|
|
while (cmd != NULL)
|
|
{
|
|
if (check_for_argument (&cmd, "-ascending", strlen ("-ascending")))
|
|
{
|
|
cmd = skip_spaces (cmd);
|
|
tp_array_compar_ascending = true;
|
|
continue;
|
|
}
|
|
|
|
if (parse_flags_qcs ("thread apply all", &cmd, &flags))
|
|
continue;
|
|
|
|
break;
|
|
}
|
|
|
|
if (cmd == NULL || *cmd == '\000')
|
|
error (_("Please specify a command at the end of 'thread apply all'"));
|
|
|
|
update_thread_list ();
|
|
|
|
int tc = live_threads_count ();
|
|
if (tc != 0)
|
|
{
|
|
/* Save a copy of the thread list and increment each thread's
|
|
refcount while executing the command in the context of each
|
|
thread, in case the command is one that wipes threads. E.g.,
|
|
detach, kill, disconnect, etc., or even normally continuing
|
|
over an inferior or thread exit. */
|
|
std::vector<thread_info *> thr_list_cpy;
|
|
thr_list_cpy.reserve (tc);
|
|
|
|
{
|
|
thread_info *tp;
|
|
|
|
ALL_NON_EXITED_THREADS (tp)
|
|
{
|
|
thr_list_cpy.push_back (tp);
|
|
}
|
|
|
|
gdb_assert (thr_list_cpy.size () == tc);
|
|
}
|
|
|
|
/* Increment the refcounts, and restore them back on scope
|
|
exit. */
|
|
scoped_inc_dec_ref inc_dec_ref (thr_list_cpy);
|
|
|
|
std::sort (thr_list_cpy.begin (), thr_list_cpy.end (), tp_array_compar);
|
|
|
|
scoped_restore_current_thread restore_thread;
|
|
|
|
for (thread_info *thr : thr_list_cpy)
|
|
if (thread_alive (thr))
|
|
thr_try_catch_cmd (thr, cmd, from_tty, flags);
|
|
}
|
|
}
|
|
|
|
/* Implementation of the "thread apply" command. */
|
|
|
|
static void
|
|
thread_apply_command (const char *tidlist, int from_tty)
|
|
{
|
|
qcs_flags flags;
|
|
const char *cmd = NULL;
|
|
const char *cmd_or_flags;
|
|
tid_range_parser parser;
|
|
|
|
if (tidlist == NULL || *tidlist == '\000')
|
|
error (_("Please specify a thread ID list"));
|
|
|
|
parser.init (tidlist, current_inferior ()->num);
|
|
while (!parser.finished ())
|
|
{
|
|
int inf_num, thr_start, thr_end;
|
|
|
|
if (!parser.get_tid_range (&inf_num, &thr_start, &thr_end))
|
|
{
|
|
cmd = parser.cur_tok ();
|
|
break;
|
|
}
|
|
}
|
|
|
|
cmd_or_flags = cmd;
|
|
while (cmd != NULL && parse_flags_qcs ("thread apply", &cmd, &flags))
|
|
;
|
|
|
|
if (cmd == NULL)
|
|
error (_("Please specify a command following the thread ID list"));
|
|
|
|
if (tidlist == cmd || !isalpha (cmd[0]))
|
|
invalid_thread_id_error (cmd);
|
|
|
|
scoped_restore_current_thread restore_thread;
|
|
|
|
parser.init (tidlist, current_inferior ()->num);
|
|
while (!parser.finished () && parser.cur_tok () < cmd_or_flags)
|
|
{
|
|
struct thread_info *tp = NULL;
|
|
struct inferior *inf;
|
|
int inf_num, thr_num;
|
|
|
|
parser.get_tid (&inf_num, &thr_num);
|
|
inf = find_inferior_id (inf_num);
|
|
if (inf != NULL)
|
|
tp = find_thread_id (inf, thr_num);
|
|
|
|
if (parser.in_star_range ())
|
|
{
|
|
if (inf == NULL)
|
|
{
|
|
warning (_("Unknown inferior %d"), inf_num);
|
|
parser.skip_range ();
|
|
continue;
|
|
}
|
|
|
|
/* No use looking for threads past the highest thread number
|
|
the inferior ever had. */
|
|
if (thr_num >= inf->highest_thread_num)
|
|
parser.skip_range ();
|
|
|
|
/* Be quiet about unknown threads numbers. */
|
|
if (tp == NULL)
|
|
continue;
|
|
}
|
|
|
|
if (tp == NULL)
|
|
{
|
|
if (show_inferior_qualified_tids () || parser.tid_is_qualified ())
|
|
warning (_("Unknown thread %d.%d"), inf_num, thr_num);
|
|
else
|
|
warning (_("Unknown thread %d"), thr_num);
|
|
continue;
|
|
}
|
|
|
|
if (!thread_alive (tp))
|
|
{
|
|
warning (_("Thread %s has terminated."), print_thread_id (tp));
|
|
continue;
|
|
}
|
|
|
|
thr_try_catch_cmd (tp, cmd, from_tty, flags);
|
|
}
|
|
}
|
|
|
|
|
|
/* Implementation of the "taas" command. */
|
|
|
|
static void
|
|
taas_command (const char *cmd, int from_tty)
|
|
{
|
|
std::string expanded = std::string ("thread apply all -s ") + cmd;
|
|
execute_command (expanded.c_str (), from_tty);
|
|
}
|
|
|
|
/* Implementation of the "tfaas" command. */
|
|
|
|
static void
|
|
tfaas_command (const char *cmd, int from_tty)
|
|
{
|
|
std::string expanded
|
|
= std::string ("thread apply all -s frame apply all -s ") + cmd;
|
|
execute_command (expanded.c_str (), from_tty);
|
|
}
|
|
|
|
/* Switch to the specified thread, or print the current thread. */
|
|
|
|
void
|
|
thread_command (const char *tidstr, int from_tty)
|
|
{
|
|
if (tidstr == NULL)
|
|
{
|
|
if (inferior_ptid == null_ptid)
|
|
error (_("No thread selected"));
|
|
|
|
if (target_has_stack)
|
|
{
|
|
struct thread_info *tp = inferior_thread ();
|
|
|
|
if (tp->state == THREAD_EXITED)
|
|
printf_filtered (_("[Current thread is %s (%s) (exited)]\n"),
|
|
print_thread_id (tp),
|
|
target_pid_to_str (inferior_ptid));
|
|
else
|
|
printf_filtered (_("[Current thread is %s (%s)]\n"),
|
|
print_thread_id (tp),
|
|
target_pid_to_str (inferior_ptid));
|
|
}
|
|
else
|
|
error (_("No stack."));
|
|
}
|
|
else
|
|
{
|
|
ptid_t previous_ptid = inferior_ptid;
|
|
|
|
thread_select (tidstr, parse_thread_id (tidstr, NULL));
|
|
|
|
/* Print if the thread has not changed, otherwise an event will
|
|
be sent. */
|
|
if (inferior_ptid == previous_ptid)
|
|
{
|
|
print_selected_thread_frame (current_uiout,
|
|
USER_SELECTED_THREAD
|
|
| USER_SELECTED_FRAME);
|
|
}
|
|
else
|
|
{
|
|
gdb::observers::user_selected_context_changed.notify
|
|
(USER_SELECTED_THREAD | USER_SELECTED_FRAME);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Implementation of `thread name'. */
|
|
|
|
static void
|
|
thread_name_command (const char *arg, int from_tty)
|
|
{
|
|
struct thread_info *info;
|
|
|
|
if (inferior_ptid == null_ptid)
|
|
error (_("No thread selected"));
|
|
|
|
arg = skip_spaces (arg);
|
|
|
|
info = inferior_thread ();
|
|
xfree (info->name);
|
|
info->name = arg ? xstrdup (arg) : NULL;
|
|
}
|
|
|
|
/* Find thread ids with a name, target pid, or extra info matching ARG. */
|
|
|
|
static void
|
|
thread_find_command (const char *arg, int from_tty)
|
|
{
|
|
struct thread_info *tp;
|
|
const char *tmp;
|
|
unsigned long match = 0;
|
|
|
|
if (arg == NULL || *arg == '\0')
|
|
error (_("Command requires an argument."));
|
|
|
|
tmp = re_comp (arg);
|
|
if (tmp != 0)
|
|
error (_("Invalid regexp (%s): %s"), tmp, arg);
|
|
|
|
update_thread_list ();
|
|
for (tp = thread_list; tp; tp = tp->next)
|
|
{
|
|
if (tp->name != NULL && re_exec (tp->name))
|
|
{
|
|
printf_filtered (_("Thread %s has name '%s'\n"),
|
|
print_thread_id (tp), tp->name);
|
|
match++;
|
|
}
|
|
|
|
tmp = target_thread_name (tp);
|
|
if (tmp != NULL && re_exec (tmp))
|
|
{
|
|
printf_filtered (_("Thread %s has target name '%s'\n"),
|
|
print_thread_id (tp), tmp);
|
|
match++;
|
|
}
|
|
|
|
tmp = target_pid_to_str (tp->ptid);
|
|
if (tmp != NULL && re_exec (tmp))
|
|
{
|
|
printf_filtered (_("Thread %s has target id '%s'\n"),
|
|
print_thread_id (tp), tmp);
|
|
match++;
|
|
}
|
|
|
|
tmp = target_extra_thread_info (tp);
|
|
if (tmp != NULL && re_exec (tmp))
|
|
{
|
|
printf_filtered (_("Thread %s has extra info '%s'\n"),
|
|
print_thread_id (tp), tmp);
|
|
match++;
|
|
}
|
|
}
|
|
if (!match)
|
|
printf_filtered (_("No threads match '%s'\n"), arg);
|
|
}
|
|
|
|
/* Print notices when new threads are attached and detached. */
|
|
int print_thread_events = 1;
|
|
static void
|
|
show_print_thread_events (struct ui_file *file, int from_tty,
|
|
struct cmd_list_element *c, const char *value)
|
|
{
|
|
fprintf_filtered (file,
|
|
_("Printing of thread events is %s.\n"),
|
|
value);
|
|
}
|
|
|
|
/* See gdbthread.h. */
|
|
|
|
void
|
|
thread_select (const char *tidstr, thread_info *tp)
|
|
{
|
|
if (!thread_alive (tp))
|
|
error (_("Thread ID %s has terminated."), tidstr);
|
|
|
|
switch_to_thread (tp);
|
|
|
|
annotate_thread_changed ();
|
|
|
|
/* Since the current thread may have changed, see if there is any
|
|
exited thread we can now delete. */
|
|
prune_threads ();
|
|
}
|
|
|
|
/* Print thread and frame switch command response. */
|
|
|
|
void
|
|
print_selected_thread_frame (struct ui_out *uiout,
|
|
user_selected_what selection)
|
|
{
|
|
struct thread_info *tp = inferior_thread ();
|
|
|
|
if (selection & USER_SELECTED_THREAD)
|
|
{
|
|
if (uiout->is_mi_like_p ())
|
|
{
|
|
uiout->field_int ("new-thread-id",
|
|
inferior_thread ()->global_num);
|
|
}
|
|
else
|
|
{
|
|
uiout->text ("[Switching to thread ");
|
|
uiout->field_string ("new-thread-id", print_thread_id (tp));
|
|
uiout->text (" (");
|
|
uiout->text (target_pid_to_str (inferior_ptid));
|
|
uiout->text (")]");
|
|
}
|
|
}
|
|
|
|
if (tp->state == THREAD_RUNNING)
|
|
{
|
|
if (selection & USER_SELECTED_THREAD)
|
|
uiout->text ("(running)\n");
|
|
}
|
|
else if (selection & USER_SELECTED_FRAME)
|
|
{
|
|
if (selection & USER_SELECTED_THREAD)
|
|
uiout->text ("\n");
|
|
|
|
if (has_stack_frames ())
|
|
print_stack_frame_to_uiout (uiout, get_selected_frame (NULL),
|
|
1, SRC_AND_LOC, 1);
|
|
}
|
|
}
|
|
|
|
/* Update the 'threads_executing' global based on the threads we know
|
|
about right now. */
|
|
|
|
static void
|
|
update_threads_executing (void)
|
|
{
|
|
struct thread_info *tp;
|
|
|
|
threads_executing = 0;
|
|
ALL_NON_EXITED_THREADS (tp)
|
|
{
|
|
if (tp->executing)
|
|
{
|
|
threads_executing = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
update_thread_list (void)
|
|
{
|
|
target_update_thread_list ();
|
|
update_threads_executing ();
|
|
}
|
|
|
|
/* Return a new value for the selected thread's id. Return a value of
|
|
0 if no thread is selected. If GLOBAL is true, return the thread's
|
|
global number. Otherwise return the per-inferior number. */
|
|
|
|
static struct value *
|
|
thread_num_make_value_helper (struct gdbarch *gdbarch, int global)
|
|
{
|
|
int int_val;
|
|
|
|
if (inferior_ptid == null_ptid)
|
|
int_val = 0;
|
|
else
|
|
{
|
|
thread_info *tp = inferior_thread ();
|
|
if (global)
|
|
int_val = tp->global_num;
|
|
else
|
|
int_val = tp->per_inf_num;
|
|
}
|
|
|
|
return value_from_longest (builtin_type (gdbarch)->builtin_int, int_val);
|
|
}
|
|
|
|
/* Return a new value for the selected thread's per-inferior thread
|
|
number. Return a value of 0 if no thread is selected, or no
|
|
threads exist. */
|
|
|
|
static struct value *
|
|
thread_id_per_inf_num_make_value (struct gdbarch *gdbarch,
|
|
struct internalvar *var,
|
|
void *ignore)
|
|
{
|
|
return thread_num_make_value_helper (gdbarch, 0);
|
|
}
|
|
|
|
/* Return a new value for the selected thread's global id. Return a
|
|
value of 0 if no thread is selected, or no threads exist. */
|
|
|
|
static struct value *
|
|
global_thread_id_make_value (struct gdbarch *gdbarch, struct internalvar *var,
|
|
void *ignore)
|
|
{
|
|
return thread_num_make_value_helper (gdbarch, 1);
|
|
}
|
|
|
|
/* Commands with a prefix of `thread'. */
|
|
struct cmd_list_element *thread_cmd_list = NULL;
|
|
|
|
/* Implementation of `thread' variable. */
|
|
|
|
static const struct internalvar_funcs thread_funcs =
|
|
{
|
|
thread_id_per_inf_num_make_value,
|
|
NULL,
|
|
NULL
|
|
};
|
|
|
|
/* Implementation of `gthread' variable. */
|
|
|
|
static const struct internalvar_funcs gthread_funcs =
|
|
{
|
|
global_thread_id_make_value,
|
|
NULL,
|
|
NULL
|
|
};
|
|
|
|
void
|
|
_initialize_thread (void)
|
|
{
|
|
static struct cmd_list_element *thread_apply_list = NULL;
|
|
|
|
add_info ("threads", info_threads_command,
|
|
_("Display currently known threads.\n\
|
|
Usage: info threads [-gid] [ID]...\n\
|
|
-gid: Show global thread IDs.\n\
|
|
If ID is given, it is a space-separated list of IDs of threads to display.\n\
|
|
Otherwise, all threads are displayed."));
|
|
|
|
add_prefix_cmd ("thread", class_run, thread_command, _("\
|
|
Use this command to switch between threads.\n\
|
|
The new thread ID must be currently known."),
|
|
&thread_cmd_list, "thread ", 1, &cmdlist);
|
|
|
|
#define THREAD_APPLY_FLAGS_HELP "\
|
|
Prints per-inferior thread number and target system's thread id\n\
|
|
followed by COMMAND output.\n\
|
|
FLAG arguments are -q (quiet), -c (continue), -s (silent).\n\
|
|
Flag -q disables printing the thread information.\n\
|
|
By default, if a COMMAND raises an error, thread apply is aborted.\n\
|
|
Flag -c indicates to print the error and continue.\n\
|
|
Flag -s indicates to silently ignore a COMMAND that raises an error\n\
|
|
or produces no output."
|
|
|
|
add_prefix_cmd ("apply", class_run, thread_apply_command,
|
|
_("Apply a command to a list of threads.\n\
|
|
Usage: thread apply ID... [FLAG]... COMMAND\n\
|
|
ID is a space-separated list of IDs of threads to apply COMMAND on.\n"
|
|
THREAD_APPLY_FLAGS_HELP),
|
|
&thread_apply_list, "thread apply ", 1, &thread_cmd_list);
|
|
|
|
add_cmd ("all", class_run, thread_apply_all_command,
|
|
_("\
|
|
Apply a command to all threads.\n\
|
|
\n\
|
|
Usage: thread apply all [-ascending] [FLAG]... COMMAND\n\
|
|
-ascending: Call COMMAND for all threads in ascending order.\n\
|
|
The default is descending order.\n"
|
|
THREAD_APPLY_FLAGS_HELP),
|
|
&thread_apply_list);
|
|
|
|
add_com ("taas", class_run, taas_command, _("\
|
|
Apply a command to all threads (ignoring errors and empty output).\n\
|
|
Usage: taas COMMAND\n\
|
|
shortcut for 'thread apply all -s COMMAND'"));
|
|
|
|
add_com ("tfaas", class_run, tfaas_command, _("\
|
|
Apply a command to all frames of all threads (ignoring errors and empty output).\n\
|
|
Usage: tfaas COMMAND\n\
|
|
shortcut for 'thread apply all -s frame apply all -s COMMAND'"));
|
|
|
|
add_cmd ("name", class_run, thread_name_command,
|
|
_("Set the current thread's name.\n\
|
|
Usage: thread name [NAME]\n\
|
|
If NAME is not given, then any existing name is removed."), &thread_cmd_list);
|
|
|
|
add_cmd ("find", class_run, thread_find_command, _("\
|
|
Find threads that match a regular expression.\n\
|
|
Usage: thread find REGEXP\n\
|
|
Will display thread ids whose name, target ID, or extra info matches REGEXP."),
|
|
&thread_cmd_list);
|
|
|
|
add_com_alias ("t", "thread", class_run, 1);
|
|
|
|
add_setshow_boolean_cmd ("thread-events", no_class,
|
|
&print_thread_events, _("\
|
|
Set printing of thread events (such as thread start and exit)."), _("\
|
|
Show printing of thread events (such as thread start and exit)."), NULL,
|
|
NULL,
|
|
show_print_thread_events,
|
|
&setprintlist, &showprintlist);
|
|
|
|
create_internalvar_type_lazy ("_thread", &thread_funcs, NULL);
|
|
create_internalvar_type_lazy ("_gthread", >hread_funcs, NULL);
|
|
}
|