ocaml/debugger/time_travel.ml

682 lines
22 KiB
OCaml

(**************************************************************************)
(* *)
(* OCaml *)
(* *)
(* Jerome Vouillon, projet Cristal, INRIA Rocquencourt *)
(* OCaml 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 GNU Lesser General Public License version 2.1, with the *)
(* special exception on linking described in the file LICENSE. *)
(* *)
(**************************************************************************)
(**************************** Time travel ******************************)
open Int64ops
open Instruct
open Events
open Debugcom
open Primitives
open Checkpoints
open Breakpoints
open Trap_barrier
open Input_handling
open Debugger_config
open Program_loading
open Question
exception Current_checkpoint_lost
exception Current_checkpoint_lost_start_at of int64 * int64
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: " ^ (Int.to_string checkpoint.c_pid));
if not checkpoint.c_valid then
wait_for_connection checkpoint;
current_checkpoint := checkpoint;
let dead_frags = List.filter (fun frag ->
not (List.mem frag checkpoint.c_code_fragments))
(Symbols.code_fragments ())
in
List.iter Symbols.erase_symbols dead_frags;
set_current_connection checkpoint.c_fd
(* Kill `checkpoint'. *)
let kill_checkpoint checkpoint =
if !debug_time_travel then
prerr_endline ("Kill: " ^ (Int.to_string 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. ***)
(* Separate 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 (t ++ t) before in
after::l
in
let (after, before) = cut (t0 -- _1) l in
after::(cut2_t0 t before)
(* Separate first elements and last element of a list of checkpoints. *)
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
(* Separate 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
(List.merge (fun t1 t2 -> compare t2.c_time t1.c_time) 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 is 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;
c_code_fragments = checkpoint.c_code_fragments}
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: " ^ Int.to_string 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
(* Information about last breakpoint encountered *)
let last_breakpoint = ref None
(* Last debug info loaded *)
let last_debug_info = ref None
let rec do_go_dynlink steps =
match do_go steps with
| { rep_type = Code_loaded frag; rep_event_count = steps } as report ->
begin match !last_debug_info with
| Some di ->
Symbols.add_symbols frag di;
Symbols.set_all_events frag;
last_debug_info := None
| None -> assert false
end;
if !break_on_load then report
else do_go_dynlink steps
| { rep_type = Code_unloaded frag; rep_event_count = steps } ->
Symbols.erase_symbols frag;
do_go_dynlink steps
| { rep_type = Debug_info di; rep_event_count = steps } ->
last_debug_info := Some (Array.to_list di);
do_go_dynlink steps
| report -> report
(* 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);
Symbols.update_current_event ();
begin match !current_event with
None -> find_event ()
| Some _ -> ()
end
| {rep_type = Trap_barrier} ->
(* 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_dynlink _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_dynlink duration in
!current_checkpoint.c_report <- Some report;
!current_checkpoint.c_state <- C_stopped;
!current_checkpoint.c_code_fragments <- Symbols.code_fragments ();
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} ->
Printf.printf "%d(%Ld)%s " pid time
(if valid then "" else "(invalid)"))
!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;
c_code_fragments = [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: " ^(Int.to_string 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 =
List.find (function c -> c.c_pid = pid) (!current_checkpoint::!checkpoints)
in
if pid > 0 then begin
Printf.eprintf "Lost connection with process %d" pid;
let kont =
if checkpoint == !current_checkpoint then begin
Printf.eprintf " (active process)\n";
match !current_checkpoint.c_state with
C_stopped ->
Printf.eprintf "at time %Ld" !current_checkpoint.c_time;
fun () -> raise Current_checkpoint_lost
| C_running duration ->
Printf.eprintf "between time %Ld and time %Ld"
!current_checkpoint.c_time
(!current_checkpoint.c_time ++ duration);
fun () -> raise (Current_checkpoint_lost_start_at
(!current_checkpoint.c_time, duration))
end
else ignore in
Printf.eprintf "\n"; flush stderr;
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;
kont ()
end
(* 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 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} = find_checkpoint_before (pre64 time_max)
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 the program backward from 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 breakpoint, or after `duration' steps. *)
let step duration =
if duration >= _0 then
step_forward duration
else
step_backward (_0 -- duration)
(*** Next, finish. ***)
(* Finish current function. *)
let finish () =
Symbols.update_current_event ();
match !current_event with
None ->
prerr_endline "`finish' not meaningful in outermost frame.";
raise Toplevel
| Some {ev_ev={ev_stacksize}} ->
set_initial_frame();
let (frame, pc) = up_frame 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 () =
Symbols.update_current_event ();
match !current_event with
None -> (* Beginning of the program. *)
step _1
| Some {ev_ev={ev_stacksize=ev_stacksize1}} ->
let (frame1, _pc1) = initial_frame() in
step _1;
if not !interrupted then begin
Symbols.update_current_event ();
match !current_event with
None -> ()
| Some {ev_ev={ev_stacksize=ev_stacksize2}} ->
let (frame2, _pc2) = initial_frame() in
(* Call `finish' if we've entered a function. *)
if frame1 >= 0 && frame2 >= 0 &&
frame2 - ev_stacksize2 > frame1 - ev_stacksize1
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 () =
Symbols.update_current_event ();
match !current_event with
None ->
prerr_endline "`start not meaningful in outermost frame.";
raise Toplevel
| Some {ev_ev={ev_stacksize}} ->
let (frame, _) = initial_frame() in
let (frame', pc) = up_frame 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_ev = {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 with pos = pc.pos - 4 * offset } in
while
exec_with_temporary_breakpoint pc back_run;
match !last_breakpoint with
Some (pc', frame') when pc = pc' ->
step _minus1;
(not !interrupted)
&&
(frame' - nargs > frame - ev_stacksize)
| _ ->
false
do
()
done
let previous_1 () =
Symbols.update_current_event ();
match !current_event with
None -> (* End of the program. *)
step _minus1
| Some {ev_ev={ev_stacksize=ev_stacksize1}} ->
let (frame1, _pc1) = initial_frame() in
step _minus1;
if not !interrupted then begin
Symbols.update_current_event ();
match !current_event with
None -> ()
| Some {ev_ev={ev_stacksize=ev_stacksize2}} ->
let (frame2, _pc2) = initial_frame() in
(* Call `start' if we've entered a function. *)
if frame1 >= 0 && frame2 >= 0 &&
frame2 - ev_stacksize2 > frame1 - ev_stacksize1
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)