ocaml/debugger/time_travel.ml

647 lines
20 KiB
OCaml

(***********************************************************************)
(* *)
(* Objective Caml *)
(* *)
(* Jerome Vouillon, projet Cristal, INRIA Rocquencourt *)
(* Objective Caml port by John Malecki and Xavier Leroy *)
(* *)
(* Copyright 1996 Institut National de Recherche en Informatique et *)
(* en Automatique. All rights reserved. This file is distributed *)
(* under the terms of the Q Public License version 1.0. *)
(* *)
(***********************************************************************)
(* $Id$ *)
(**************************** Time travel ******************************)
open Instruct
open Events
open Debugcom
open Primitives
open Checkpoints
open Breakpoints
open Trap_barrier
open Input_handling
open Debugger_config
open Program_loading
exception Current_checkpoint_lost
let remove_1st key list =
let rec remove =
function
[] -> []
| a::l -> if a == key then l else a::(remove l)
in
remove list
(*** Debugging. ***)
let debug_time_travel = ref false
(*** Internal utilities. ***)
(* Insert a checkpoint in the checkpoint list.
* Raise `Exit' if there is already a checkpoint at the same time.
*)
let insert_checkpoint ({c_time = time} as checkpoint) =
let rec traverse =
function
[] -> [checkpoint]
| (({c_time = t} as a)::l) as l' ->
if t > time then
a::(traverse l)
else if t = time then
raise Exit
else
checkpoint::l'
in
checkpoints := traverse !checkpoints
(* Remove a checkpoint from the checkpoint list.
* --- No error if not found.
*)
let remove_checkpoint checkpoint =
checkpoints := remove_1st checkpoint !checkpoints
(* Wait for the process used by `checkpoint' to connect.
* --- Usually not called (the process is already connected).
*)
let wait_for_connection checkpoint =
try
Exec.unprotect
(function () ->
let old_controller = Input_handling.current_controller !connection in
execute_with_other_controller
(function
fd ->
old_controller fd;
if checkpoint.c_valid = true then
exit_main_loop ())
!connection
main_loop)
with
Sys.Break ->
checkpoint.c_parent <- root;
remove_checkpoint checkpoint;
checkpoint.c_pid <- -1;
raise Sys.Break
(* Select a checkpoint as current. *)
let set_current_checkpoint checkpoint =
if !debug_time_travel then
prerr_endline ("Select : " ^ (string_of_int checkpoint.c_pid));
if not checkpoint.c_valid then
wait_for_connection checkpoint;
current_checkpoint := checkpoint;
set_current_connection checkpoint.c_fd
(* Kill `checkpoint'. *)
let kill_checkpoint checkpoint =
if !debug_time_travel then
prerr_endline ("Kill : " ^ (string_of_int checkpoint.c_pid));
if checkpoint.c_pid > 0 then (* Ghosts don't have to be killed ! *)
(if not checkpoint.c_valid then
wait_for_connection checkpoint;
stop checkpoint.c_fd;
if checkpoint.c_parent.c_pid > 0 then
wait_child checkpoint.c_parent.c_fd;
checkpoint.c_parent <- root;
close_io checkpoint.c_fd;
remove_file checkpoint.c_fd;
remove_checkpoint checkpoint);
checkpoint.c_pid <- -1 (* Don't exist anymore *)
(*** Cleaning the checkpoint list. ***)
(* Separe checkpoints before (<=) and after (>) `t'. *)
(* ### t checkpoints -> (after, before) *)
let cut t =
let rec cut_t =
function
[] -> ([], [])
| ({c_time = t'} as a::l) as l' ->
if t' <= t then
([], l')
else
let (b, e) = cut_t l in
(a::b, e)
in
cut_t
(* Partition the checkpoints list. *)
let cut2 t0 t l =
let rec cut2_t0 t =
function
[] -> []
| l ->
let (after, before) = cut (t0 - t - 1) l in
let l = cut2_t0 (2 * t) before in
after::l
in
let (after, before) = cut (t0 - 1) l in
after::(cut2_t0 t before)
(* Separe first elements and last element of a list of checkpoint. *)
let chk_merge2 cont =
let rec chk_merge2_cont =
function
[] -> cont
| [a] ->
let (accepted, rejected) = cont in
(a::accepted, rejected)
| a::l ->
let (accepted, rejected) = chk_merge2_cont l in
(accepted, a::rejected)
in chk_merge2_cont
(* Separe the checkpoint list. *)
(* ### list -> accepted * rejected *)
let rec chk_merge =
function
[] -> ([], [])
| l::tail ->
chk_merge2 (chk_merge tail) l
let new_checkpoint_list checkpoint_count accepted rejected =
if List.length accepted >= checkpoint_count then
let (k, l) = list_truncate2 checkpoint_count accepted in
(k, l @ rejected)
else
let (k, l) =
list_truncate2 (checkpoint_count - List.length accepted) rejected
in
(Sort.merge (fun {c_time = t1} {c_time = t2} -> t1 > t2) accepted k,
l)
(* Clean the checkpoint list. *)
(* Reference time is `time'. *)
let clean_checkpoints time checkpoint_count =
let (after, before) = cut time !checkpoints in
let (accepted, rejected) =
chk_merge (cut2 time !checkpoint_small_step before)
in
let (kept, lost) =
new_checkpoint_list checkpoint_count accepted after
in
List.iter kill_checkpoint (lost @ rejected);
checkpoints := kept
(*** Internal functions for moving. ***)
(* Find the first checkpoint before (or at) `time'.
* Ask for reloading the program if necessary.
*)
let find_checkpoint_before time =
let rec find =
function
[] ->
print_string "Can't go that far in the past !"; print_newline ();
if yes_or_no "Reload program" then begin
load_program ();
find !checkpoints
end
else
raise Toplevel
| { c_time = t } as a::l ->
if t > time then
find l
else
a
in find !checkpoints
(* Make a copy of the current checkpoint and clean the checkpoint list. *)
(* --- The new checkpoint in not put in the list. *)
let duplicate_current_checkpoint () =
let checkpoint = !current_checkpoint in
if not checkpoint.c_valid then
wait_for_connection checkpoint;
let new_checkpoint = (* Ghost *)
{c_time = checkpoint.c_time;
c_pid = 0;
c_fd = checkpoint.c_fd;
c_valid = false;
c_report = checkpoint.c_report;
c_state = C_stopped;
c_parent = checkpoint;
c_breakpoint_version = checkpoint.c_breakpoint_version;
c_breakpoints = checkpoint.c_breakpoints;
c_trap_barrier = checkpoint.c_trap_barrier}
in
checkpoints := list_replace checkpoint new_checkpoint !checkpoints;
set_current_checkpoint checkpoint;
clean_checkpoints (checkpoint.c_time + 1) (!checkpoint_max_count - 1);
if new_checkpoint.c_pid = 0 then (* The ghost has not been killed *)
(match do_checkpoint () with (* Duplicate checkpoint *)
Checkpoint_done pid ->
(new_checkpoint.c_pid <- pid;
if !debug_time_travel then
prerr_endline ("Waiting for connection : " ^ (string_of_int pid)))
| Checkpoint_failed ->
prerr_endline
"A fork failed. Reducing maximum number of checkpoints.";
checkpoint_max_count := List.length !checkpoints - 1;
remove_checkpoint new_checkpoint)
(* Was the movement interrupted ? *)
(* --- An exception could have been used instead, *)
(* --- but it is not clear where it should be caught. *)
(* --- For instance, it should not be caught in `step' *)
(* --- (as `step' is used in `next_1'). *)
(* --- On the other side, other modules does not need to know *)
(* --- about this exception. *)
let interrupted = ref false
(* Informations about last breakpoint encountered *)
let last_breakpoint = ref None
(* Ensure we stop on an event. *)
let rec stop_on_event report =
match report with
{rep_type = Breakpoint; rep_program_pointer = pc;
rep_stack_pointer = sp} ->
last_breakpoint := Some (pc, sp);
update_current_event ();
begin match !current_event with
None -> find_event ()
| Some _ -> ()
end
| {rep_type = Trap_barrier; rep_stack_pointer = trap_frame} ->
(* No event at current position. *)
find_event ()
| _ ->
()
and find_event () =
if !debug_time_travel then begin
print_string "Searching next event...";
print_newline ()
end;
let report = do_go 1 in
!current_checkpoint.c_report <- Some report;
stop_on_event report
(* Internal function for running debugged program.
* Requires `duration > 0'.
*)
let internal_step duration =
match current_report () with
Some {rep_type = Exited | Uncaught_exc} -> ()
| _ ->
Exec.protect
(function () ->
if !make_checkpoints then
duplicate_current_checkpoint ()
else
remove_checkpoint !current_checkpoint;
update_breakpoints ();
update_trap_barrier ();
!current_checkpoint.c_state <- C_running duration;
let report = do_go duration in
!current_checkpoint.c_report <- Some report;
!current_checkpoint.c_state <- C_stopped;
if report.rep_type = Event then begin
!current_checkpoint.c_time <-
!current_checkpoint.c_time + duration;
interrupted := false;
last_breakpoint := None
end
else begin
!current_checkpoint.c_time <-
!current_checkpoint.c_time + duration
- report.rep_event_count + 1;
interrupted := true;
last_breakpoint := None;
stop_on_event report
end;
(try
insert_checkpoint !current_checkpoint
with
Exit ->
kill_checkpoint !current_checkpoint;
set_current_checkpoint
(find_checkpoint_before (current_time ()))));
if !debug_time_travel then begin
print_string "Checkpoints : pid(time)"; print_newline ();
List.iter
(function {c_time = time; c_pid = pid; c_valid = valid} ->
print_int pid;
print_string "("; print_int time; print_string ")";
if not valid then print_string "(invalid)";
print_string " ")
!checkpoints;
print_newline ()
end
(*** Miscellaneous functions (exported). ***)
(* Create a checkpoint at time 0 (new program). *)
let new_checkpoint pid fd =
let new_checkpoint =
{c_time = 0;
c_pid = pid;
c_fd = fd;
c_valid = true;
c_report = None;
c_state = C_stopped;
c_parent = root;
c_breakpoint_version = 0;
c_breakpoints = [];
c_trap_barrier = 0}
in
insert_checkpoint new_checkpoint
(* Set the file descriptor of a checkpoint *)
(* (a new process has connected with the debugger). *)
(* --- Return `true' on success (close the connection otherwise). *)
let set_file_descriptor pid fd =
let rec find =
function
[] ->
prerr_endline "Unexpected connection";
close_io fd;
false
| ({c_pid = pid'} as checkpoint)::l ->
if pid <> pid' then
find l
else
(checkpoint.c_fd <- fd;
checkpoint.c_valid <- true;
true)
in
if !debug_time_travel then
prerr_endline ("New connection : " ^(string_of_int pid));
find (!current_checkpoint::!checkpoints)
(* Kill all the checkpoints. *)
let kill_all_checkpoints () =
List.iter kill_checkpoint (!current_checkpoint::!checkpoints)
(* Kill a checkpoint without killing the process. *)
(* (used when connection with the process is lost). *)
(* --- Assume that the checkpoint is valid. *)
let forget_process fd pid =
let checkpoint =
find (function c -> c.c_pid = pid) (!current_checkpoint::!checkpoints)
in
prerr_string "Lost connection with process ";
prerr_int pid;
if checkpoint = !current_checkpoint then begin
prerr_endline " (active process)";
match !current_checkpoint.c_state with
C_stopped ->
prerr_string "at time ";
prerr_int !current_checkpoint.c_time
| C_running duration ->
prerr_string "between time ";
prerr_int !current_checkpoint.c_time;
prerr_string " and time ";
prerr_int (!current_checkpoint.c_time + duration)
end;
prerr_endline "";
Input_handling.remove_file fd;
close_io checkpoint.c_fd;
remove_file checkpoint.c_fd;
remove_checkpoint checkpoint;
checkpoint.c_pid <- -1; (* Don't exist anymore *)
if checkpoint.c_parent.c_pid > 0 then
wait_child checkpoint.c_parent.c_fd;
if checkpoint = !current_checkpoint then
raise Current_checkpoint_lost
(* Try to recover when the current checkpoint is lost. *)
let recover () =
set_current_checkpoint
(find_checkpoint_before (current_time ()))
(*** Simple movements. ***)
(* Forward stepping. Requires `duration >= 0'. *)
let rec step_forward duration =
if duration > !checkpoint_small_step then begin
let first_step =
if duration > !checkpoint_big_step then
!checkpoint_big_step
else
!checkpoint_small_step
in
internal_step first_step;
if not !interrupted then
step_forward (duration - first_step)
end
else if duration != 0 then
internal_step duration
(* Go to time `time' from current checkpoint (internal). *)
let internal_go_to time =
let duration = time - current_time () in
if duration > 0 then
execute_without_breakpoints (function () -> step_forward duration)
(* Move to a given time. *)
let go_to time =
let checkpoint = find_checkpoint_before time in
set_current_checkpoint checkpoint;
internal_go_to time
(* Return the time of the last breakpoint *)
(* between current time and `max_time'. *)
let rec find_last_breakpoint max_time =
let rec find break =
let time = current_time () in
step_forward (max_time - time);
match !last_breakpoint, !temporary_breakpoint_position with
(Some _, _) when current_time () < max_time ->
find !last_breakpoint
| (Some (pc, _), Some pc') when pc = pc' ->
(max_time, !last_breakpoint)
| _ ->
(time, break)
in
find
(match current_pc_sp () with
(Some (pc, _)) as state when breakpoint_at_pc pc -> state
| _ -> None)
(* Run from `time_max' back to `time'. *)
(* --- Assume 0 <= time < time_max *)
let rec back_to time time_max =
let
{c_time = t} as checkpoint = find_checkpoint_before (time_max - 1)
in
go_to (max time t);
let (new_time, break) = find_last_breakpoint time_max in
if break <> None || (new_time <= time) then begin
go_to new_time;
interrupted := break <> None;
last_breakpoint := break
end else
back_to time new_time
(* Backward stepping. *)
(* --- Assume duration > 1 *)
let step_backward duration =
let time = current_time () in
if time > 0 then
back_to (max 0 (time - duration)) time
(* Run the program from current time. *)
(* Stop at the first breakpoint, or at the end of the program. *)
let rec run () =
internal_step !checkpoint_big_step;
if not !interrupted then
run ()
(* Run backward the program form current time. *)
(* Stop at the first breakpoint, or at the beginning of the program. *)
let back_run () =
if current_time () > 0 then
back_to 0 (current_time ())
(* Step in any direction. *)
(* Stop at the first brakpoint, or after `duration' steps. *)
let step duration =
if duration >= 0 then
step_forward duration
else
step_backward (-duration)
(*** Next, finish. ***)
(* Finish current function. *)
let finish () =
update_current_event ();
match !current_event with
None ->
prerr_endline "`finish' not meaningful in outermost frame.";
raise Toplevel
| Some curr_event ->
set_initial_frame();
let (frame, pc) = up_frame curr_event.ev_stacksize in
if frame < 0 then begin
prerr_endline "`finish' not meaningful in outermost frame.";
raise Toplevel
end;
begin
try ignore(Symbols.any_event_at_pc pc)
with Not_found ->
prerr_endline "Calling function has no debugging information.";
raise Toplevel
end;
exec_with_trap_barrier
frame
(fun () ->
exec_with_temporary_breakpoint
pc
(fun () ->
while
run ();
match !last_breakpoint with
Some (pc', frame') when pc = pc' ->
interrupted := false;
frame <> frame'
| _ ->
false
do
()
done))
let next_1 () =
update_current_event ();
match !current_event with
None -> (* Beginning of the program. *)
step 1
| Some event1 ->
let (frame1, pc1) = initial_frame() in
step 1;
if not !interrupted then begin
update_current_event ();
match !current_event with
None -> ()
| Some event2 ->
let (frame2, pc2) = initial_frame() in
(* Call `finish' if we've entered a function. *)
if frame1 >= 0 && frame2 >= 0 &&
frame2 - event2.ev_stacksize > frame1 - event1.ev_stacksize
then finish()
end
(* Same as `step' (forward) but skip over function calls. *)
let rec next =
function
0 -> ()
| n ->
next_1 ();
if not !interrupted then
next (n - 1)
(* Run backward until just before current function. *)
let start () =
update_current_event ();
match !current_event with
None ->
prerr_endline "`start not meaningful in outermost frame.";
raise Toplevel
| Some curr_event ->
let (frame, _) = initial_frame() in
let (frame', pc) = up_frame curr_event.ev_stacksize in
if frame' < 0 then begin
prerr_endline "`start not meaningful in outermost frame.";
raise Toplevel
end;
let nargs =
match
try Symbols.any_event_at_pc pc with Not_found ->
prerr_endline "Calling function has no debugging information.";
raise Toplevel
with
{ev_info = Event_return nargs} -> nargs
| _ -> Misc.fatal_error "Time_travel.start"
in
let offset = if nargs < 4 then 1 else 2 in
let pc = pc - 4 * offset in
while
exec_with_temporary_breakpoint pc back_run;
match !last_breakpoint with
Some (pc', frame') when pc = pc' ->
step (-1);
(not !interrupted)
&&
(frame' - nargs > frame - curr_event.ev_stacksize)
| _ ->
false
do
()
done
let previous_1 () =
update_current_event ();
match !current_event with
None -> (* End of the program. *)
step (-1)
| Some event1 ->
let (frame1, pc1) = initial_frame() in
step (-1);
if not !interrupted then begin
update_current_event ();
match !current_event with
None -> ()
| Some event2 ->
let (frame2, pc2) = initial_frame() in
(* Call `start' if we've entered a function. *)
if frame1 >= 0 && frame2 >= 0 &&
frame2 - event2.ev_stacksize > frame1 - event1.ev_stacksize
then start()
end
(* Same as `step' (backward) but skip over function calls. *)
let rec previous =
function
0 -> ()
| n ->
previous_1 ();
if not !interrupted then
previous (n - 1)