Skip to content

Commit 0c2a329

Browse files
equivalence1dmitrii.kravchenko
authored andcommitted
added interaction with gdb, added c-level attaching
1 parent 645aab6 commit 0c2a329

File tree

5 files changed

+266
-44
lines changed

5 files changed

+266
-44
lines changed

bin/gdb_wrapper

Lines changed: 209 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
require 'optparse'
44
require 'ostruct'
55

6+
$stdout.sync = true
7+
$stderr.sync = true
8+
69
options = OpenStruct.new(
7-
'pid' => nil,
8-
'sdk_path' => nil,
9-
'uid' => nil,
10-
'gems_to_include' => []
10+
'pid' => nil,
11+
'sdk_path' => nil,
12+
'uid' => nil,
13+
'gems_to_include' => []
1114
)
1215

1316
opts = OptionParser.new do |opts|
@@ -16,82 +19,244 @@ opts = OptionParser.new do |opts|
1619
Some useful banner.
1720
EOB
1821

19-
opts.on("--pid PID", "pid of process you want to attach to for debugging") do |pid|
22+
opts.on('--pid PID', 'pid of process you want to attach to for debugging') do |pid|
2023
options.pid = pid
2124
end
2225

23-
opts.on("--ruby-path SDK_PATH", "path to ruby interpreter") do |ruby_path|
26+
opts.on('--ruby-path RUBY_PATH', 'path to ruby interpreter') do |ruby_path|
2427
options.ruby_path = ruby_path
2528
end
2629

27-
opts.on("--uid UID", "uid which this process should set after executing gdb attach") do |uid|
30+
opts.on('--uid UID', 'uid which this process should set after executing gdb attach') do |uid|
2831
options.uid = uid
2932
end
3033

31-
opts.on("--include-gem GEM_LIB_PATH", "lib of gem to include") do |gem_lib_path|
34+
opts.on('--include-gem GEM_LIB_PATH', 'lib of gem to include') do |gem_lib_path|
3235
options.gems_to_include << gem_lib_path
3336
end
3437
end
3538

3639
opts.parse! ARGV
3740

3841
unless options.pid
39-
$stderr.puts "You must specify PID of process you want to attach to"
42+
$stderr.puts 'You should specify PID of process you want to attach to'
4043
exit 1
4144
end
4245

4346
unless options.ruby_path
44-
$stderr.puts "You must specify RUBY_PATH of ruby interpreter"
47+
$stderr.puts 'You should specify path to the ruby interpreter'
4548
exit 1
4649
end
4750

48-
# TODO Denis told not to implement this hack
49-
# So this is only for me while debugging as
50-
# I don't want to get any warnings.
51-
sigints_caught = 0
52-
trap('INT') do
53-
sigints_caught += 1
54-
if sigints_caught == 2
55-
exit 0
56-
end
57-
end
58-
5951
argv = '["' + ARGV * '", "' + '"]'
6052
gems_to_include = '["' + options.gems_to_include * '", "' + '"]'
6153

62-
commands_list = []
54+
path_to_debugger_loader = File.expand_path(File.dirname(__FILE__)) + '/../lib/ruby-debug-ide/attach/debugger_loader'
6355

64-
def commands_list.<<(command)
65-
self.push "-ex \"#{command}\""
56+
options.gems_to_include.each do |gem_path|
57+
$LOAD_PATH.unshift(gem_path) unless $LOAD_PATH.include?(gem_path)
6658
end
6759

68-
path_to_debugger_loader = File.expand_path(File.dirname(__FILE__)) + '/../lib/ruby-debug-ide/attach/debugger_loader'
60+
require 'ruby-debug-ide/greeter'
61+
Debugger::print_greeting_msg(nil, nil)
6962

70-
# rb_finish: wait while execution comes to the next line.
71-
# This is essential because we could interrupt process in a middle
72-
# of some evaluations (e.g., system call)
73-
commands_list << "call rb_eval_string_protect(\\\"set_trace_func lambda{|event, file, line, id, binding, classname| if /line/ =~ event; sleep 0; set_trace_func(nil); end}\\\", (int *)0)"
74-
commands_list << "tbreak rb_f_sleep"
75-
commands_list << "cont"
63+
$pid = options.pid
64+
$last_bt = ''
65+
$gdb_tmp_file = '/tmp/gdb_out.txt'
7666

77-
# evalr: loading debugger into the process
78-
evalr = "call rb_eval_string_protect(%s, (int *)0)"
79-
commands_list << ("#{evalr}" % ["(\\\"require '#{path_to_debugger_loader}'; load_debugger(#{gems_to_include.gsub("\"", "'")}, #{argv.gsub("\"", "'")})\\\")"])
67+
begin
68+
file = File.open($gdb_tmp_file, 'w')
69+
file.truncate(0)
70+
file.close
71+
rescue Exception => e
72+
$stderr.puts e
73+
$stderr.puts "Could not create file #{$gdb_tmp_file} for gdb logging. Aborting."
74+
exit!
75+
end
8076

81-
# q: exit gdb and continue process execution with debugger
82-
commands_list << "q"
77+
gdb_executed_all_commands = false
8378

84-
cmd = "gdb #{options.ruby_path} #{options.pid} -nh -nx -batch #{commands_list.join(" ")}"
79+
IO.popen("gdb #{options.ruby_path} #{options.pid} -nh -nx", 'r+') do |gdb|
8580

86-
options.gems_to_include.each do |gem_path|
87-
$LOAD_PATH.unshift(gem_path) unless $LOAD_PATH.include?(gem_path)
88-
end
81+
$gdb = gdb
82+
$main_thread = nil
8983

90-
require 'ruby-debug-ide/greeter'
91-
Debugger::print_greeting_msg(nil, nil)
92-
$stderr.puts "Running command #{cmd}"
84+
class ProcessThread
85+
86+
attr_reader :thread_num, :is_main
87+
88+
def initialize(thread_num, is_main)
89+
@thread_num = thread_num
90+
@is_main = is_main
91+
end
92+
93+
def switch
94+
$gdb.execute "thread #{thread_num}"
95+
end
96+
97+
def finish
98+
$gdb.finish
99+
end
100+
101+
def get_bt
102+
return $gdb.execute 'bt'
103+
end
104+
105+
def top_caller_match(bt, pattern)
106+
return bt.split('#')[1] =~ /#{pattern}/
107+
end
108+
109+
def any_caller_match(bt, pattern)
110+
return bt =~ /#{pattern}/
111+
end
112+
113+
def is_inside_malloc(bt = get_bt)
114+
if any_caller_match(bt, '(malloc\.c)')
115+
$stderr.puts "process #{$pid} is currently inside malloc."
116+
return true
117+
else
118+
return false
119+
end
120+
end
121+
122+
def is_inside_gc(bt = get_bt)
123+
if any_caller_match(bt, '(gc\.c)')
124+
$stderr.puts "process #{$pid} is currently in garbage collection phase."
125+
return true
126+
else
127+
return false
128+
end
129+
end
130+
131+
def need_finish_frame
132+
bt = get_bt
133+
return is_inside_malloc(bt) || is_inside_gc(bt)
134+
end
135+
136+
end
137+
138+
def gdb.update_threads
139+
process_threads = []
140+
info_threads = (self.execute 'info threads').split("\n")
141+
# first line of gdb's response is ` Id Target Id Frame` info line
142+
# last line of gdb's response is `(gdb) `
143+
info_threads.shift
144+
info_threads.pop
145+
# each thread info looks like this:
146+
# 3 Thread 0x7ff535405700 (LWP 8291) "ruby-timer-thr" 0x00007ff534a15fdd in poll () at ../sysdeps/unix/syscall-template.S:81
147+
info_threads.each do |thread_info|
148+
next unless thread_info =~ /[\s*]*\d+\s+Thread.*/
149+
$stderr.puts "thread_info: #{thread_info}"
150+
is_main = thread_info[0] == '*'
151+
thread_info.sub!(/[\s*]*/, '')
152+
thread_info.sub!(/\s.*$/, '')
153+
thread = ProcessThread.new(thread_info.to_i, is_main)
154+
if thread.is_main
155+
$main_thread = thread
156+
end
157+
process_threads << thread
158+
end
159+
process_threads
160+
end
161+
162+
def gdb.get_response
163+
content = ''
164+
loop do
165+
sleep 0.01 # give time to gdb to finish command execution and print it to file
166+
file = File.open($gdb_tmp_file, 'r')
167+
content = file.read
168+
file.close
169+
break if content =~ /\(gdb\)\s\z/
170+
end
171+
content
172+
end
173+
174+
def gdb.enable_logging
175+
self.puts 'set logging on'
176+
end
177+
178+
def gdb.disable_logging
179+
self.puts 'set logging off'
180+
end
181+
182+
def gdb.overwrite_file
183+
disable_logging
184+
enable_logging
185+
end
186+
187+
def gdb.execute(command)
188+
self.overwrite_file
189+
self.puts command
190+
$stdout.puts "executed command '#{command}' inside gdb."
191+
if command == 'q'
192+
return ''
193+
end
194+
response = self.get_response
195+
if command == 'bt'
196+
$last_bt = response
197+
end
198+
return response
199+
end
200+
201+
def gdb.finish
202+
$stdout.puts 'trying to finish current frame.'
203+
self.execute 'finish'
204+
end
205+
206+
def gdb.set_logging
207+
self.puts "set logging file #{$gdb_tmp_file}"
208+
self.puts 'set logging overwrite on'
209+
self.puts 'set logging redirect on'
210+
self.enable_logging
211+
212+
$stdout.puts "all gdb output redirected to #{$gdb_tmp_file}."
213+
end
214+
215+
def gdb.check_already_under_debug
216+
threads = self.execute 'info threads'
217+
return threads =~ /ruby-debug-ide/
218+
end
93219

94-
`#{cmd}` or raise "GDB failed. Aborting."
220+
gdb.set_logging
221+
222+
if gdb.check_already_under_debug
223+
$stderr.puts "Process #{$pid} is already under debug"
224+
gdb.execute 'q'
225+
end
226+
227+
gdb.execute 'set scheduler-locking off'
228+
gdb.execute 'set unwindonsignal on'
229+
230+
should_check_threads_state = true
231+
232+
while should_check_threads_state
233+
should_check_threads_state = false
234+
gdb.update_threads.each do |thread|
235+
thread.switch
236+
while thread.need_finish_frame
237+
should_check_threads_state = true
238+
thread.finish
239+
end
240+
end
241+
end
242+
243+
$main_thread.switch
244+
245+
gdb.execute "call dlopen(\"/home/equi/Job/JetBrains/Internship_2016/ruby-debug-ide/ext/libAttach.so\", 2)"
246+
gdb.execute "call start_attach(\"require '#{path_to_debugger_loader}'; load_debugger(#{gems_to_include.gsub("\"", "'")}, #{argv.gsub("\"", "'")})\")"
247+
248+
gdb_executed_all_commands = true
249+
gdb.execute 'q'
250+
251+
end
252+
253+
trap('INT') do
254+
unless gdb_executed_all_commands
255+
$stderr.puts "Seems like could not attach to process. Its backtrace:\n#{$last_bt}"
256+
$stderr.flush
257+
end
258+
exit 1
259+
end
95260

96261
if options.uid
97262
Process::Sys.setuid(options.uid.to_i)

ext/Makefile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
all: libAttach.so
2+
3+
libAttach.so: libAttach.o
4+
gcc -shared -o libAttach.so libAttach.o
5+
6+
libAttach.o: do_attach.c
7+
gcc -Wall -g -fPIC -c -I/home/equi/.rvm/rubies/ruby-2.3.1/include/ruby-2.3.0 -I/home/equi/.rvm/rubies/ruby-2.3.1/include/ruby-2.3.0/x86_64-linux/ do_attach.c -o libAttach.o
8+
9+
clean:
10+
rm libAttach.*

ext/do_attach.c

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#include "do_attach.h"
2+
3+
static const char *_command_to_eval;
4+
5+
static int
6+
__check_gc(void)
7+
{
8+
if (rb_during_gc()) {
9+
fprintf(stderr, "Can not connect during garbage collection phase. Please, try again later.\n");
10+
return 1;
11+
}
12+
return 0;
13+
}
14+
15+
static void
16+
__catch_line_event(rb_event_flag_t evflag, VALUE data, VALUE self, ID mid, VALUE klass)
17+
{
18+
(void)sizeof(evflag);
19+
(void)sizeof(self);
20+
(void)sizeof(mid);
21+
(void)sizeof(klass);
22+
23+
rb_remove_event_hook(__catch_line_event);
24+
if (__check_gc())
25+
return;
26+
rb_eval_string_protect(_command_to_eval, NULL); // TODO pass something more useful than NULL
27+
}
28+
29+
void
30+
start_attach(const char* command)
31+
{
32+
_command_to_eval = command;
33+
if (__check_gc())
34+
return;
35+
rb_global_variable((VALUE *) _command_to_eval);
36+
rb_add_event_hook(__catch_line_event, RUBY_EVENT_LINE, (VALUE) NULL);
37+
}

ext/do_attach.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#ifndef __DO_ATTACH_H__
2+
#define __DO_ATTACH_H__
3+
4+
#include <ruby.h>
5+
#include <ruby/debug.h>
6+
#include <stdio.h>
7+
8+
void start_attach(const char *command);
9+
10+
#endif //__DO_ATTACH_H__

lib/ruby-debug-ide/attach/empty_file.rb

Whitespace-only changes.

0 commit comments

Comments
 (0)