1 // Copyright 2020 the V8 project authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "src/debug/wasm/gdb-server/target.h"
6 
7 #include <inttypes.h>
8 #include "src/base/platform/time.h"
9 #include "src/debug/wasm/gdb-server/gdb-remote-util.h"
10 #include "src/debug/wasm/gdb-server/gdb-server.h"
11 #include "src/debug/wasm/gdb-server/packet.h"
12 #include "src/debug/wasm/gdb-server/session.h"
13 #include "src/debug/wasm/gdb-server/transport.h"
14 
15 namespace v8 {
16 namespace internal {
17 namespace wasm {
18 namespace gdb_server {
19 
20 static const int kThreadId = 1;
21 
22 // Signals.
23 static const int kSigTrace = 5;
24 static const int kSigSegv = 11;
25 
Target(GdbServer * gdb_server)26 Target::Target(GdbServer* gdb_server)
27     : gdb_server_(gdb_server),
28       status_(Status::Running),
29       cur_signal_(0),
30       session_(nullptr),
31       debugger_initial_suspension_(true),
32       semaphore_(0),
33       current_isolate_(nullptr) {
34   InitQueryPropertyMap();
35 }
36 
InitQueryPropertyMap()37 void Target::InitQueryPropertyMap() {
38   // Request LLDB to send packets up to 4000 bytes for bulk transfers.
39   query_properties_["Supported"] =
40       "PacketSize=1000;vContSupported-;qXfer:libraries:read+;";
41 
42   query_properties_["Attached"] = "1";
43 
44   // There is only one register, named 'pc', in this architecture
45   query_properties_["RegisterInfo0"] =
46       "name:pc;alt-name:pc;bitsize:64;offset:0;encoding:uint;format:hex;set:"
47       "General Purpose Registers;gcc:16;dwarf:16;generic:pc;";
48   query_properties_["RegisterInfo1"] = "E45";
49 
50   // ProcessInfo for wasm32
51   query_properties_["ProcessInfo"] =
52       "pid:1;ppid:1;uid:1;gid:1;euid:1;egid:1;name:6c6c6462;triple:" +
53       Mem2Hex("wasm32-unknown-unknown-wasm") + ";ptrsize:4;";
54   query_properties_["Symbol"] = "OK";
55 
56   // Current thread info
57   char buff[16];
58   snprintf(buff, sizeof(buff), "QC%x", kThreadId);
59   query_properties_["C"] = buff;
60 }
61 
Terminate()62 void Target::Terminate() {
63   // Executed in the Isolate thread, when the process shuts down.
64   SetStatus(Status::Terminated);
65 }
66 
OnProgramBreak(Isolate * isolate,const std::vector<wasm_addr_t> & call_frames)67 void Target::OnProgramBreak(Isolate* isolate,
68                             const std::vector<wasm_addr_t>& call_frames) {
69   OnSuspended(isolate, kSigTrace, call_frames);
70 }
OnException(Isolate * isolate,const std::vector<wasm_addr_t> & call_frames)71 void Target::OnException(Isolate* isolate,
72                          const std::vector<wasm_addr_t>& call_frames) {
73   OnSuspended(isolate, kSigSegv, call_frames);
74 }
OnSuspended(Isolate * isolate,int signal,const std::vector<wasm_addr_t> & call_frames)75 void Target::OnSuspended(Isolate* isolate, int signal,
76                          const std::vector<wasm_addr_t>& call_frames) {
77   // This function will be called in the isolate thread, when the wasm
78   // interpreter gets suspended.
79 
80   bool isWaitingForSuspension = (status_ == Status::WaitingForSuspension);
81   SetStatus(Status::Suspended, signal, call_frames, isolate);
82   if (isWaitingForSuspension) {
83     // Wake the GdbServer thread that was blocked waiting for the Target
84     // to suspend.
85     semaphore_.Signal();
86   } else if (session_) {
87     session_->SignalThreadEvent();
88   }
89 }
90 
Run(Session * session)91 void Target::Run(Session* session) {
92   // Executed in the GdbServer thread.
93   session_ = session;
94   do {
95     WaitForDebugEvent();
96     ProcessDebugEvent();
97     ProcessCommands();
98   } while (!IsTerminated() && session_->IsConnected());
99   session_ = nullptr;
100 }
101 
WaitForDebugEvent()102 void Target::WaitForDebugEvent() {
103   // Executed in the GdbServer thread.
104 
105   if (status_ == Status::Running) {
106     // Wait for either:
107     //   * the thread to fault (or single-step)
108     //   * an interrupt from LLDB
109     session_->WaitForDebugStubEvent();
110   }
111 }
112 
ProcessDebugEvent()113 void Target::ProcessDebugEvent() {
114   // Executed in the GdbServer thread
115 
116   if (status_ == Status::Running) {
117     // Blocks, waiting for the engine to suspend.
118     Suspend();
119   }
120 
121   // Here, the wasm interpreter has suspended and we have updated the current
122   // thread info.
123 
124   if (debugger_initial_suspension_) {
125     // First time on a connection, we don't send the signal.
126     // All other times, send the signal that triggered us.
127     debugger_initial_suspension_ = false;
128   } else {
129     Packet pktOut;
130     SetStopReply(&pktOut);
131     session_->SendPacket(&pktOut, false);
132   }
133 }
134 
Suspend()135 void Target::Suspend() {
136   // Executed in the GdbServer thread
137   if (status_ == Status::Running) {
138     // TODO(paolosev) - this only suspends the wasm interpreter.
139     gdb_server_->Suspend();
140 
141     status_ = Status::WaitingForSuspension;
142   }
143 
144   while (status_ == Status::WaitingForSuspension) {
145     if (semaphore_.WaitFor(base::TimeDelta::FromMilliseconds(500))) {
146       // Here the wasm interpreter is suspended.
147       return;
148     }
149   }
150 }
151 
ProcessCommands()152 void Target::ProcessCommands() {
153   // GDB-remote messages are processed in the GDBServer thread.
154 
155   if (IsTerminated()) {
156     return;
157   } else if (status_ != Status::Suspended) {
158     // Don't process commands if we haven't stopped.
159     return;
160   }
161 
162   // Now we are ready to process commands.
163   // Loop through packets until we process a continue packet or a detach.
164   Packet recv, reply;
165   while (session_->IsConnected()) {
166     if (!session_->GetPacket(&recv)) {
167       continue;
168     }
169 
170     reply.Clear();
171     ProcessPacketResult result = ProcessPacket(&recv, &reply);
172     switch (result) {
173       case ProcessPacketResult::Paused:
174         session_->SendPacket(&reply);
175         break;
176 
177       case ProcessPacketResult::Continue:
178         DCHECK_EQ(status_, Status::Running);
179         // If this is a continue type command, break out of this loop.
180         gdb_server_->QuitMessageLoopOnPause();
181         return;
182 
183       case ProcessPacketResult::Detach:
184         SetStatus(Status::Running);
185         session_->SendPacket(&reply);
186         session_->Disconnect();
187         gdb_server_->QuitMessageLoopOnPause();
188         return;
189 
190       case ProcessPacketResult::Kill:
191         session_->SendPacket(&reply);
192         exit(-9);
193 
194       default:
195         UNREACHABLE();
196     }
197   }
198 
199   if (!session_->IsConnected()) {
200     debugger_initial_suspension_ = true;
201   }
202 }
203 
ProcessPacket(Packet * pkt_in,Packet * pkt_out)204 Target::ProcessPacketResult Target::ProcessPacket(Packet* pkt_in,
205                                                   Packet* pkt_out) {
206   ErrorCode err = ErrorCode::None;
207 
208   // Clear the outbound message.
209   pkt_out->Clear();
210 
211   // Set the sequence number, if present.
212   int32_t seq = -1;
213   if (pkt_in->GetSequence(&seq)) {
214     pkt_out->SetSequence(seq);
215   }
216 
217   // A GDB-remote packet begins with an upper- or lower-case letter, which
218   // generally represents a single command.
219   // The letters 'q' and 'Q' introduce a "General query packets" and are used
220   // to extend the protocol with custom commands.
221   // The format of GDB-remote commands is documented here:
222   // https://sourceware.org/gdb/onlinedocs/gdb/Overview.html#Overview.
223   char cmd;
224   pkt_in->GetRawChar(&cmd);
225 
226   switch (cmd) {
227     // Queries the reason the target halted.
228     // IN : $?
229     // OUT: A Stop-reply packet
230     case '?':
231       SetStopReply(pkt_out);
232       break;
233 
234     // Resumes execution
235     // IN : $c
236     // OUT: A Stop-reply packet is sent later, when the execution halts.
237     case 'c':
238       SetStatus(Status::Running);
239       return ProcessPacketResult::Continue;
240 
241     // Detaches the debugger from this target
242     // IN : $D
243     // OUT: $OK
244     case 'D':
245       TRACE_GDB_REMOTE("Requested Detach.\n");
246       pkt_out->AddString("OK");
247       return ProcessPacketResult::Detach;
248 
249     // Read general registers (We only support register 'pc' that contains
250     // the current instruction pointer).
251     // IN : $g
252     // OUT: $xx...xx
253     case 'g': {
254       uint64_t pc = GetCurrentPc();
255       pkt_out->AddBlock(&pc, sizeof(pc));
256       break;
257     }
258 
259     // Write general registers - NOT SUPPORTED
260     // IN : $Gxx..xx
261     // OUT: $ (empty string)
262     case 'G': {
263       break;
264     }
265 
266     // Set thread for subsequent operations. For Wasm targets, we currently
267     // assume that there is only one thread with id = kThreadId (= 1).
268     // IN : $H(c/g)(-1,0,xxxx)
269     // OUT: $OK
270     case 'H': {
271       // Type of the operation (‘m’, ‘M’, ‘g’, ‘G’, ...)
272       char operation;
273       if (!pkt_in->GetRawChar(&operation)) {
274         err = ErrorCode::BadFormat;
275         break;
276       }
277 
278       uint64_t thread_id;
279       if (!pkt_in->GetNumberSep(&thread_id, 0)) {
280         err = ErrorCode::BadFormat;
281         break;
282       }
283 
284       // Ignore, only one thread supported for now.
285       pkt_out->AddString("OK");
286       break;
287     }
288 
289     // Kills the debuggee.
290     // IN : $k
291     // OUT: $OK
292     case 'k':
293       TRACE_GDB_REMOTE("Requested Kill.\n");
294       pkt_out->AddString("OK");
295       return ProcessPacketResult::Kill;
296 
297     // Reads {llll} addressable memory units starting at address {aaaa}.
298     // IN : $maaaa,llll
299     // OUT: $xx..xx
300     case 'm': {
301       uint64_t address;
302       if (!pkt_in->GetNumberSep(&address, 0)) {
303         err = ErrorCode::BadFormat;
304         break;
305       }
306       wasm_addr_t wasm_addr(address);
307 
308       uint64_t len;
309       if (!pkt_in->GetNumberSep(&len, 0)) {
310         err = ErrorCode::BadFormat;
311         break;
312       }
313 
314       if (len > Transport::kBufSize / 2) {
315         err = ErrorCode::BadArgs;
316         break;
317       }
318 
319       uint32_t length = static_cast<uint32_t>(len);
320       uint8_t buff[Transport::kBufSize];
321       if (wasm_addr.ModuleId() > 0) {
322         uint32_t read =
323             gdb_server_->GetWasmModuleBytes(wasm_addr, buff, length);
324         if (read > 0) {
325           pkt_out->AddBlock(buff, read);
326         } else {
327           err = ErrorCode::Failed;
328         }
329       } else {
330         err = ErrorCode::BadArgs;
331       }
332       break;
333     }
334 
335     // Writes {llll} addressable memory units starting at address {aaaa}.
336     // IN : $Maaaa,llll:xx..xx
337     // OUT: $OK
338     case 'M': {
339       // Writing to memory not supported for Wasm.
340       err = ErrorCode::Failed;
341       break;
342     }
343 
344     // pN: Reads the value of register N.
345     // IN : $pxx
346     // OUT: $xx..xx
347     case 'p': {
348       uint64_t pc = GetCurrentPc();
349       pkt_out->AddBlock(&pc, sizeof(pc));
350     } break;
351 
352     case 'q': {
353       err = ProcessQueryPacket(pkt_in, pkt_out);
354       break;
355     }
356 
357     // Single step
358     // IN : $s
359     // OUT: A Stop-reply packet is sent later, when the execution halts.
360     case 's': {
361       if (status_ == Status::Suspended) {
362         gdb_server_->PrepareStep();
363         SetStatus(Status::Running);
364       }
365       return ProcessPacketResult::Continue;
366     }
367 
368     // Find out if the thread 'id' is alive.
369     // IN : $T
370     // OUT: $OK if alive, $Enn if thread is dead.
371     case 'T': {
372       uint64_t id;
373       if (!pkt_in->GetNumberSep(&id, 0)) {
374         err = ErrorCode::BadFormat;
375         break;
376       }
377       if (id != kThreadId) {
378         err = ErrorCode::BadArgs;
379         break;
380       }
381       pkt_out->AddString("OK");
382       break;
383     }
384 
385     // Z: Adds a breakpoint
386     // IN : $Z<type>,<addr>,<kind>
387     //      <type>: 0: sw breakpoint, 1: hw breakpoint, 2: watchpoint
388     // OUT: $OK (success) or $Enn (error)
389     case 'Z': {
390       uint64_t breakpoint_type;
391       uint64_t breakpoint_address;
392       uint64_t breakpoint_kind;
393       // Only software breakpoints are supported.
394       if (!pkt_in->GetNumberSep(&breakpoint_type, 0) || breakpoint_type != 0 ||
395           !pkt_in->GetNumberSep(&breakpoint_address, 0) ||
396           !pkt_in->GetNumberSep(&breakpoint_kind, 0)) {
397         err = ErrorCode::BadFormat;
398         break;
399       }
400 
401       wasm_addr_t wasm_breakpoint_addr(breakpoint_address);
402       if (!gdb_server_->AddBreakpoint(wasm_breakpoint_addr.ModuleId(),
403                                       wasm_breakpoint_addr.Offset())) {
404         err = ErrorCode::Failed;
405         break;
406       }
407 
408       pkt_out->AddString("OK");
409       break;
410     }
411 
412     // z: Removes a breakpoint
413     // IN : $z<type>,<addr>,<kind>
414     //      <type>: 0: sw breakpoint, 1: hw breakpoint, 2: watchpoint
415     // OUT: $OK (success) or $Enn (error)
416     case 'z': {
417       uint64_t breakpoint_type;
418       uint64_t breakpoint_address;
419       uint64_t breakpoint_kind;
420       if (!pkt_in->GetNumberSep(&breakpoint_type, 0) || breakpoint_type != 0 ||
421           !pkt_in->GetNumberSep(&breakpoint_address, 0) ||
422           !pkt_in->GetNumberSep(&breakpoint_kind, 0)) {
423         err = ErrorCode::BadFormat;
424         break;
425       }
426 
427       wasm_addr_t wasm_breakpoint_addr(breakpoint_address);
428       if (!gdb_server_->RemoveBreakpoint(wasm_breakpoint_addr.ModuleId(),
429                                          wasm_breakpoint_addr.Offset())) {
430         err = ErrorCode::Failed;
431         break;
432       }
433 
434       pkt_out->AddString("OK");
435       break;
436     }
437 
438     // If the command is not recognized, ignore it by sending an empty reply.
439     default: {
440       TRACE_GDB_REMOTE("Unknown command: %s\n", pkt_in->GetPayload());
441     }
442   }
443 
444   // If there is an error, return the error code instead of a payload
445   if (err != ErrorCode::None) {
446     pkt_out->Clear();
447     pkt_out->AddRawChar('E');
448     pkt_out->AddWord8(static_cast<uint8_t>(err));
449   }
450   return ProcessPacketResult::Paused;
451 }
452 
ProcessQueryPacket(const Packet * pkt_in,Packet * pkt_out)453 Target::ErrorCode Target::ProcessQueryPacket(const Packet* pkt_in,
454                                              Packet* pkt_out) {
455   const char* str = &pkt_in->GetPayload()[1];
456 
457   // Get first thread query
458   // IN : $qfThreadInfo
459   // OUT: $m<tid>
460   //
461   // Get next thread query
462   // IN : $qsThreadInfo
463   // OUT: $m<tid> or l to denote end of list.
464   if (!strcmp(str, "fThreadInfo") || !strcmp(str, "sThreadInfo")) {
465     if (str[0] == 'f') {
466       pkt_out->AddString("m");
467       pkt_out->AddNumberSep(kThreadId, 0);
468     } else {
469       pkt_out->AddString("l");
470     }
471     return ErrorCode::None;
472   }
473 
474   // Get a list of loaded libraries
475   // IN : $qXfer:libraries:read
476   // OUT: an XML document which lists loaded libraries, with this format:
477   // <library-list>
478   //   <library name="foo.wasm">
479   //     <section address="0x100000000"/>
480   //   </library>
481   //   <library name="bar.wasm">
482   //     <section address="0x200000000"/>
483   //   </library>
484   // </library-list>
485   // Note that LLDB must be compiled with libxml2 support to handle this packet.
486   std::string tmp = "Xfer:libraries:read";
487   if (!strncmp(str, tmp.data(), tmp.length())) {
488     std::vector<GdbServer::WasmModuleInfo> modules =
489         gdb_server_->GetLoadedModules();
490     std::string result("l<library-list>");
491     for (const auto& module : modules) {
492       wasm_addr_t address(module.module_id, 0);
493       char address_string[32];
494       snprintf(address_string, sizeof(address_string), "%" PRIu64,
495                static_cast<uint64_t>(address));
496       result += "<library name=\"";
497       result += module.module_name;
498       result += "\"><section address=\"";
499       result += address_string;
500       result += "\"/></library>";
501     }
502     result += "</library-list>";
503     pkt_out->AddString(result.c_str());
504     return ErrorCode::None;
505   }
506 
507   // Get the current call stack.
508   // IN : $qWasmCallStack
509   // OUT: $xx..xxyy..yyzz..zz (A sequence of uint64_t values represented as
510   //                           consecutive 8-bytes blocks).
511   std::vector<std::string> toks = StringSplit(str, ":;");
512   if (toks[0] == "WasmCallStack") {
513     std::vector<wasm_addr_t> call_stack_pcs = gdb_server_->GetWasmCallStack();
514     std::vector<uint64_t> buffer;
515     for (wasm_addr_t pc : call_stack_pcs) {
516       buffer.push_back(pc);
517     }
518     pkt_out->AddBlock(buffer.data(),
519                       static_cast<uint32_t>(sizeof(uint64_t) * buffer.size()));
520     return ErrorCode::None;
521   }
522 
523   // Get a Wasm global value in the Wasm module specified.
524   // IN : $qWasmGlobal:frame_index;index
525   // OUT: $xx..xx
526   if (toks[0] == "WasmGlobal") {
527     if (toks.size() == 3) {
528       uint32_t frame_index =
529           static_cast<uint32_t>(strtol(toks[1].data(), nullptr, 10));
530       uint32_t index =
531           static_cast<uint32_t>(strtol(toks[2].data(), nullptr, 10));
532       uint8_t buff[16];
533       uint32_t size = 0;
534       if (gdb_server_->GetWasmGlobal(frame_index, index, buff, 16, &size)) {
535         pkt_out->AddBlock(buff, size);
536         return ErrorCode::None;
537       } else {
538         return ErrorCode::Failed;
539       }
540     }
541     return ErrorCode::BadFormat;
542   }
543 
544   // Get a Wasm local value in the stack frame specified.
545   // IN : $qWasmLocal:frame_index;index
546   // OUT: $xx..xx
547   if (toks[0] == "WasmLocal") {
548     if (toks.size() == 3) {
549       uint32_t frame_index =
550           static_cast<uint32_t>(strtol(toks[1].data(), nullptr, 10));
551       uint32_t index =
552           static_cast<uint32_t>(strtol(toks[2].data(), nullptr, 10));
553       uint8_t buff[16];
554       uint32_t size = 0;
555       if (gdb_server_->GetWasmLocal(frame_index, index, buff, 16, &size)) {
556         pkt_out->AddBlock(buff, size);
557         return ErrorCode::None;
558       } else {
559         return ErrorCode::Failed;
560       }
561     }
562     return ErrorCode::BadFormat;
563   }
564 
565   // Get a Wasm local from the operand stack at the index specified.
566   // IN : qWasmStackValue:frame_index;index
567   // OUT: $xx..xx
568   if (toks[0] == "WasmStackValue") {
569     if (toks.size() == 3) {
570       uint32_t frame_index =
571           static_cast<uint32_t>(strtol(toks[1].data(), nullptr, 10));
572       uint32_t index =
573           static_cast<uint32_t>(strtol(toks[2].data(), nullptr, 10));
574       uint8_t buff[16];
575       uint32_t size = 0;
576       if (gdb_server_->GetWasmStackValue(frame_index, index, buff, 16, &size)) {
577         pkt_out->AddBlock(buff, size);
578         return ErrorCode::None;
579       } else {
580         return ErrorCode::Failed;
581       }
582     }
583     return ErrorCode::BadFormat;
584   }
585 
586   // Read Wasm memory.
587   // IN : $qWasmMem:frame_index;addr;len
588   // OUT: $xx..xx
589   if (toks[0] == "WasmMem") {
590     if (toks.size() == 4) {
591       uint32_t frame_index =
592           static_cast<uint32_t>(strtol(toks[1].data(), nullptr, 10));
593       uint32_t address =
594           static_cast<uint32_t>(strtol(toks[2].data(), nullptr, 16));
595       uint32_t length =
596           static_cast<uint32_t>(strtol(toks[3].data(), nullptr, 16));
597       if (length > Transport::kBufSize / 2) {
598         return ErrorCode::BadArgs;
599       }
600       uint8_t buff[Transport::kBufSize];
601       uint32_t read =
602           gdb_server_->GetWasmMemory(frame_index, address, buff, length);
603       if (read > 0) {
604         pkt_out->AddBlock(buff, read);
605         return ErrorCode::None;
606       } else {
607         return ErrorCode::Failed;
608       }
609     }
610     return ErrorCode::BadFormat;
611   }
612 
613   // No match so far, check the property cache.
614   QueryPropertyMap::const_iterator it = query_properties_.find(toks[0]);
615   if (it != query_properties_.end()) {
616     pkt_out->AddString(it->second.data());
617   }
618   // If not found, just send an empty response.
619   return ErrorCode::None;
620 }
621 
622 // A Stop-reply packet has the format:
623 //   Sxx
624 // or:
625 //   Txx<name1>:<value1>;...;<nameN>:<valueN>
626 // where 'xx' is a two-digit hex number that represents the stop signal
627 // and the <name>:<value> pairs are used to report additional information,
628 // like the thread id.
SetStopReply(Packet * pkt_out) const629 void Target::SetStopReply(Packet* pkt_out) const {
630   pkt_out->AddRawChar('T');
631   pkt_out->AddWord8(cur_signal_);
632 
633   // Adds 'thread-pcs:<pc1>,...,<pcN>;' A list of pc values for all threads that
634   // currently exist in the process.
635   char buff[64];
636   snprintf(buff, sizeof(buff), "thread-pcs:%" PRIx64 ";",
637            static_cast<uint64_t>(GetCurrentPc()));
638   pkt_out->AddString(buff);
639 
640   // Adds 'thread:<tid>;' pair. Note that a terminating ';' is required.
641   pkt_out->AddString("thread:");
642   pkt_out->AddNumberSep(kThreadId, ';');
643 }
644 
SetStatus(Status status,int8_t signal,std::vector<wasm_addr_t> call_frames,Isolate * isolate)645 void Target::SetStatus(Status status, int8_t signal,
646                        std::vector<wasm_addr_t> call_frames, Isolate* isolate) {
647   v8::base::MutexGuard guard(&mutex_);
648 
649   DCHECK((status == Status::Suspended && signal != 0 &&
650           call_frames.size() > 0 && isolate != nullptr) ||
651          (status != Status::Suspended && signal == 0 &&
652           call_frames.size() == 0 && isolate == nullptr));
653 
654   current_isolate_ = isolate;
655   status_ = status;
656   cur_signal_ = signal;
657   call_frames_ = call_frames;
658 }
659 
GetCallStack() const660 const std::vector<wasm_addr_t> Target::GetCallStack() const {
661   v8::base::MutexGuard guard(&mutex_);
662 
663   return call_frames_;
664 }
665 
GetCurrentPc() const666 wasm_addr_t Target::GetCurrentPc() const {
667   v8::base::MutexGuard guard(&mutex_);
668 
669   wasm_addr_t pc{0};
670   if (call_frames_.size() > 0) {
671     pc = call_frames_[0];
672   }
673   return pc;
674 }
675 
676 }  // namespace gdb_server
677 }  // namespace wasm
678 }  // namespace internal
679 }  // namespace v8
680