1 : // Copyright 2012 Google Inc. All Rights Reserved.
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 : //
15 : // Implementation of the basic-block entry counting agent library.
16 :
17 : #include "syzygy/agent/basic_block_entry/basic_block_entry.h"
18 :
19 : #include "base/at_exit.h"
20 : #include "base/command_line.h"
21 : #include "base/environment.h"
22 : #include "base/lazy_instance.h"
23 : #include "base/stringprintf.h"
24 : #include "base/utf_string_conversions.h"
25 : #include "base/memory/scoped_ptr.h"
26 : #include "sawbuck/common/com_utils.h"
27 : #include "syzygy/agent/common/process_utils.h"
28 : #include "syzygy/agent/common/scoped_last_error_keeper.h"
29 : #include "syzygy/common/logging.h"
30 : #include "syzygy/trace/protocol/call_trace_defs.h"
31 :
32 : // Save caller-save registers (eax, ecx, edx) and flags (eflags).
33 : #define BBENTRY_SAVE_REGISTERS \
34 : __asm push eax \
35 : __asm lahf \
36 : __asm seto al \
37 : __asm push eax \
38 : __asm push ecx \
39 : __asm push edx
40 :
41 : // Restore caller-save registers (eax, ecx, edx) and flags (eflags).
42 : #define BBENTRY_RESTORE_REGISTERS \
43 : __asm pop edx \
44 : __asm pop ecx \
45 : __asm pop eax \
46 : __asm add al, 0x7f \
47 : __asm sahf \
48 : __asm pop eax
49 :
50 : extern "C" uint32* _stdcall GetRawFrequencyData(
51 E : ::common::IndexedFrequencyData* data) {
52 E : DCHECK(data != NULL);
53 E : return agent::basic_block_entry::BasicBlockEntry::GetRawFrequencyData(data);
54 E : }
55 :
56 i : extern "C" void __declspec(naked) _increment_indexed_freq_data() {
57 : __asm {
58 : // This is expected to be called via instrumentation that looks like:
59 : // push index
60 : // push module_data
61 : // call [_increment_indexed_freq_data]
62 : //
63 : // Stack: ... index, module_data, ret_addr.
64 :
65 : // Stash volatile registers.
66 i : BBENTRY_SAVE_REGISTERS
67 :
68 : // Stack: ... index, module_data, ret_addr, [4x register]
69 :
70 : // Push the original esp value onto the stack as the entry-hook data.
71 : // This gives the entry-hook a pointer to ret_addr, module_data and index.
72 i : lea eax, DWORD PTR[esp + 0x10]
73 i : push eax
74 :
75 : // Stack: ..., index, module_data, ret_addr, [4x register], esp, &ret_addr.
76 i : call agent::basic_block_entry::BasicBlockEntry::IncrementIndexedFreqDataHook
77 :
78 : // Stack: ... index, module_data, ret_addr, [4x register].
79 :
80 : // Restore volatile registers.
81 i : BBENTRY_RESTORE_REGISTERS
82 :
83 : // Stack: ... index, module_data, ret_addr.
84 :
85 : // Return to the address pushed by our caller, popping off the
86 : // index and module_data values from the stack.
87 i : ret 8
88 :
89 : // Stack: ...
90 : }
91 : }
92 :
93 i : extern "C" void __declspec(naked) _indirect_penter_dllmain() {
94 : __asm {
95 : // This is expected to be called via a thunk that looks like:
96 : // push module_data
97 : // push function
98 : // jmp [_indirect_penter_dllmain]
99 : //
100 : // Stack: ... reserved, reason, module, ret_addr, module_data, function.
101 :
102 : // Stash volatile registers.
103 i : BBENTRY_SAVE_REGISTERS
104 :
105 : // Stack: ... reserved, reason, module, ret_addr, module_data, function,
106 : // [4x register].
107 :
108 : // Push the original esp value onto the stack as the entry-hook data.
109 : // This gives the dll entry-hook a pointer to function, module_data,
110 : // ret_addr, module, reason and reserved.
111 i : lea eax, DWORD PTR[esp + 0x10]
112 i : push eax
113 :
114 : // Stack: ... reserved, reason, module, ret_addr, module_data, function,
115 : // [4x register], &function.
116 :
117 i : call agent::basic_block_entry::BasicBlockEntry::DllMainEntryHook
118 :
119 : // Stack: ... reserved, reason, module, ret_addr, module_data, function,
120 : // [4x register].
121 :
122 : // Restore volatile registers.
123 i : BBENTRY_RESTORE_REGISTERS
124 :
125 : // Stack: ... reserved, reason, module, ret_addr, module_data, function.
126 :
127 : // Return to the thunked function, popping module_data off the stack as
128 : // we go.
129 i : ret 4
130 :
131 : // Stack: ... reserved, reason, module, ret_addr.
132 : }
133 : }
134 :
135 i : extern "C" void __declspec(naked) _indirect_penter_exemain() {
136 : __asm {
137 : // This is expected to be called via a thunk that looks like:
138 : // push module_data
139 : // push function
140 : // jmp [_indirect_penter_exe_main]
141 : //
142 : // Stack: ... ret_addr, module_data, function.
143 :
144 : // Stash volatile registers.
145 i : BBENTRY_SAVE_REGISTERS
146 :
147 : // Stack: ... ret_addr, module_data, function, [4x register].
148 :
149 : // Push the original esp value onto the stack as the entry-hook data.
150 : // This gives the exe entry-hook a pointer to function, module_data,
151 : // and ret_addr.
152 i : lea eax, DWORD PTR[esp + 0x10]
153 i : push eax
154 :
155 : // Stack: ... ret_addr, module_data, function, [4x register], frame.
156 :
157 i : call agent::basic_block_entry::BasicBlockEntry::ExeMainEntryHook
158 :
159 : // Stack: ... ret_addr, module_data, function, [4x register].
160 :
161 : // Restore volatile registers.
162 i : BBENTRY_RESTORE_REGISTERS
163 :
164 : // Stack: ... ret_addr, module_data, function.
165 :
166 : // Return to the thunked function, popping module_data off the stack as
167 : // we go.
168 i : ret 4
169 :
170 : // Stack: ... reserved, reason, module, ret_addr.
171 : }
172 : }
173 :
174 E : BOOL WINAPI DllMain(HMODULE instance, DWORD reason, LPVOID reserved) {
175 : // Our AtExit manager required by base.
176 : static base::AtExitManager* at_exit = NULL;
177 :
178 E : switch (reason) {
179 : case DLL_PROCESS_ATTACH:
180 E : DCHECK(at_exit == NULL);
181 E : at_exit = new base::AtExitManager();
182 :
183 E : CommandLine::Init(0, NULL);
184 E : common::InitLoggingForDll(L"basic_block_entry");
185 E : LOG(INFO) << "Initialized basic-block entry counting agent library.";
186 E : break;
187 :
188 : case DLL_THREAD_ATTACH:
189 i : break;
190 :
191 : case DLL_THREAD_DETACH:
192 i : break;
193 :
194 : case DLL_PROCESS_DETACH:
195 E : DCHECK(at_exit != NULL);
196 E : delete at_exit;
197 E : at_exit = NULL;
198 E : break;
199 :
200 : default:
201 i : NOTREACHED();
202 : break;
203 : }
204 :
205 E : return TRUE;
206 E : }
207 :
208 : namespace agent {
209 : namespace basic_block_entry {
210 :
211 : namespace {
212 :
213 : using ::common::IndexedFrequencyData;
214 : using agent::common::ScopedLastErrorKeeper;
215 : using trace::client::TraceFileSegment;
216 :
217 : // All tracing runs through this object.
218 : base::LazyInstance<BasicBlockEntry> static_bbentry_instance =
219 : LAZY_INSTANCE_INITIALIZER;
220 :
221 : // Get the address of the module containing @p addr. We do this by querying
222 : // for the allocation that contains @p addr. This must lie within the
223 : // instrumented module, and be part of the single allocation in which the
224 : // image of the module lies. The base of the module will be the base address
225 : // of the allocation.
226 : // TODO(rogerm): Move to agent::common.
227 E : HMODULE GetModuleForAddr(const void* addr) {
228 E : MEMORY_BASIC_INFORMATION mem_info = {};
229 :
230 : // Lookup up the allocation in which addr is located.
231 E : if (::VirtualQuery(addr, &mem_info, sizeof(mem_info)) == 0) {
232 i : DWORD error = ::GetLastError();
233 i : LOG(ERROR) << "VirtualQuery failed: " << com::LogWe(error) << ".";
234 i : return NULL;
235 : }
236 :
237 : // Check that the allocation base has a valid PE header magic number.
238 E : base::win::PEImage image(reinterpret_cast<HMODULE>(mem_info.AllocationBase));
239 E : if (!image.VerifyMagic()) {
240 i : LOG(ERROR) << "Invalid module found for "
241 : << base::StringPrintf("0x%08X", addr) << ".";
242 i : return NULL;
243 : }
244 :
245 : // Then it's a module.
246 E : return image.module();
247 E : }
248 :
249 : // Returns true if @p version is the expected version for @p datatype_id.
250 E : bool DatatypeVersionIsValid(uint32 datatype_id, uint32 version) {
251 E : if (datatype_id == ::common::IndexedFrequencyData::BASIC_BLOCK_ENTRY)
252 E : return version == ::common::kBasicBlockFrequencyDataVersion;
253 i : else if (datatype_id == ::common::IndexedFrequencyData::JUMP_TABLE)
254 i : return version == ::common::kJumpTableFrequencyDataVersion;
255 i : return false;
256 E : }
257 :
258 : } // namespace
259 :
260 : // The IncrementIndexedFreqDataHook parameters.
261 : struct BasicBlockEntry::IncrementIndexedFreqDataFrame {
262 : const void* ret_addr;
263 : IndexedFrequencyData* module_data;
264 : uint32 index;
265 : };
266 : COMPILE_ASSERT_IS_POD_OF_SIZE(BasicBlockEntry::IncrementIndexedFreqDataFrame,
267 : 12);
268 :
269 : // The DllMainEntryHook parameters.
270 : struct BasicBlockEntry::DllMainEntryFrame {
271 : FuncAddr function;
272 : IndexedFrequencyData* module_data;
273 : const void* ret_addr;
274 : HMODULE module;
275 : DWORD reason;
276 : DWORD reserved;
277 : };
278 : COMPILE_ASSERT_IS_POD_OF_SIZE(BasicBlockEntry::DllMainEntryFrame, 24);
279 :
280 : // The ExeMainEntryHook parameters.
281 : struct BasicBlockEntry::ExeMainEntryFrame {
282 : FuncAddr function;
283 : IndexedFrequencyData* module_data;
284 : const void* ret_addr;
285 : };
286 : COMPILE_ASSERT_IS_POD_OF_SIZE(BasicBlockEntry::ExeMainEntryFrame, 12);
287 :
288 : // The per-thread-per-instrumented-module state managed by this agent.
289 : class BasicBlockEntry::ThreadState : public agent::common::ThreadStateBase {
290 : public:
291 : // Initialize a ThreadState instance.
292 : ThreadState(BasicBlockEntry* agent, void* buffer);
293 :
294 : // Destroy a ThreadState instance.
295 : ~ThreadState();
296 :
297 : // @name Accessors.
298 : // @{
299 E : uint32* frequency_data() { return frequency_data_; }
300 E : TraceFileSegment* segment() { return &segment_; }
301 : TraceIndexedFrequencyData* trace_data() { return trace_data_; }
302 : // @}
303 :
304 : // @name Mutators.
305 : // @{
306 : void set_frequency_data(void* buffer);
307 : void set_trace_data(TraceIndexedFrequencyData* trace_data);
308 : // @}
309 :
310 : // A helper to return a ThreadState pointer given a TLS index.
311 : static ThreadState* Get(DWORD tls_index);
312 :
313 : // A helper to assign a ThreadState pointer to a TLS index.
314 : void Assign(DWORD tls_index);
315 :
316 : // Saturation increment the frequency record for @p index. Note that in
317 : // Release mode, no range checking is performed on index.
318 : void Increment(uint32 index);
319 :
320 : protected:
321 : // As a shortcut, this points to the beginning of the array of basic-block
322 : // entry frequency values. With tracing enabled, this is equivalent to:
323 : // reinterpret_cast<uint32*>(this->trace_data->frequency_data)
324 : // If tracing is not enabled, this will be set to point to a static
325 : // allocation of IndexedFrequencyData::frequency_data.
326 : uint32* frequency_data_;
327 :
328 : // The basic-block entry agent this thread state belongs to.
329 : BasicBlockEntry* agent_;
330 :
331 : // The thread's current trace-file segment, if any.
332 : trace::client::TraceFileSegment segment_;
333 :
334 : // The basic-block frequency record we're populating. This will point into
335 : // the associated trace file segment's buffer.
336 : TraceIndexedFrequencyData* trace_data_;
337 :
338 : private:
339 : DISALLOW_COPY_AND_ASSIGN(ThreadState);
340 : };
341 :
342 : BasicBlockEntry::ThreadState::ThreadState(BasicBlockEntry* agent, void* buffer)
343 : : agent_(agent),
344 : frequency_data_(static_cast<uint32*>(buffer)),
345 E : trace_data_(NULL) {
346 E : DCHECK(agent != NULL);
347 E : DCHECK(buffer != NULL);
348 E : }
349 :
350 i : BasicBlockEntry::ThreadState::~ThreadState() {
351 : // If we have an outstanding buffer, let's deallocate it now.
352 i : if (segment_.write_ptr != NULL && !agent_->session_.IsDisabled())
353 i : agent_->session_.ReturnBuffer(&segment_);
354 i : }
355 :
356 E : void BasicBlockEntry::ThreadState::set_frequency_data(void* buffer) {
357 E : DCHECK(buffer != NULL);
358 E : frequency_data_ = static_cast<uint32*>(buffer);
359 E : }
360 :
361 : void BasicBlockEntry::ThreadState::set_trace_data(
362 : TraceIndexedFrequencyData* trace_data) {
363 : DCHECK(trace_data != NULL);
364 : trace_data_ = trace_data;
365 : }
366 :
367 : BasicBlockEntry::ThreadState* BasicBlockEntry::ThreadState::Get(
368 E : DWORD tls_index) {
369 E : DCHECK_NE(TLS_OUT_OF_INDEXES, tls_index);
370 E : return static_cast<ThreadState*>(::TlsGetValue(tls_index));
371 E : }
372 :
373 E : void BasicBlockEntry::ThreadState::Assign(DWORD tls_index) {
374 E : DCHECK_NE(TLS_OUT_OF_INDEXES, tls_index);
375 E : ::TlsSetValue(tls_index, this);
376 E : }
377 :
378 E : inline void BasicBlockEntry::ThreadState::Increment(uint32 index) {
379 E : DCHECK(frequency_data_ != NULL);
380 E : DCHECK(trace_data_ == NULL || index < trace_data_->num_entries);
381 E : uint32& element = frequency_data_[index];
382 E : if (element != ~0U)
383 E : ++element;
384 E : }
385 :
386 E : BasicBlockEntry* BasicBlockEntry::Instance() {
387 E : return static_bbentry_instance.Pointer();
388 E : }
389 :
390 E : BasicBlockEntry::BasicBlockEntry() {
391 : // Create a session. We immediately return the buffer that gets allocated
392 : // to us. The client will perform thread-local buffer management on an as-
393 : // needed basis.
394 E : trace::client::TraceFileSegment dummy_segment;
395 E : if (trace::client::InitializeRpcSession(&session_, &dummy_segment))
396 E : CHECK(session_.ReturnBuffer(&dummy_segment));
397 E : }
398 :
399 E : BasicBlockEntry::~BasicBlockEntry() {
400 E : }
401 :
402 E : uint32* BasicBlockEntry::GetRawFrequencyData(IndexedFrequencyData* data) {
403 E : DCHECK(data != NULL);
404 E : ThreadState* state = ThreadState::Get(data->tls_index);
405 E : if (state == NULL)
406 E : state = Instance()->CreateThreadState(data);
407 E : return state->frequency_data();
408 E : }
409 :
410 : void BasicBlockEntry::IncrementIndexedFreqDataHook(
411 E : IncrementIndexedFreqDataFrame* entry_frame) {
412 E : ScopedLastErrorKeeper scoped_last_error_keeper;
413 E : DCHECK(entry_frame != NULL);
414 E : DCHECK(entry_frame->module_data != NULL);
415 : DCHECK_GT(entry_frame->module_data->num_entries,
416 E : entry_frame->index);
417 :
418 : // TODO(rogerm): Consider extracting a fast path for state != NULL? Inline it
419 : // during instrumentation? Move it into the _increment_indexed_freq_data
420 : // function?
421 E : ThreadState* state = ThreadState::Get(entry_frame->module_data->tls_index);
422 E : if (state == NULL)
423 E : state = Instance()->CreateThreadState(entry_frame->module_data);
424 E : state->Increment(entry_frame->index);
425 E : }
426 :
427 E : void BasicBlockEntry::DllMainEntryHook(DllMainEntryFrame* entry_frame) {
428 E : ScopedLastErrorKeeper scoped_last_error_keeper;
429 E : DCHECK(entry_frame != NULL);
430 E : switch (entry_frame->reason) {
431 : case DLL_PROCESS_ATTACH:
432 E : Instance()->OnProcessAttach(entry_frame->module_data);
433 E : break;
434 :
435 : case DLL_THREAD_ATTACH:
436 : // We don't handle this event because the thread may never actually
437 : // call into an instrumented module, so we don't want to allocate
438 : // resources needlessly. Further, we won't get this event for thread
439 : // that were created before the agent was loaded. On first use of
440 : // an instrumented basic-block in a given thread, any thread specific
441 : // resources will be allocated.
442 i : break;
443 :
444 : case DLL_PROCESS_DETACH:
445 : case DLL_THREAD_DETACH:
446 E : Instance()->OnThreadDetach(entry_frame->module_data);
447 E : break;
448 :
449 : default:
450 i : NOTREACHED();
451 : }
452 E : }
453 :
454 E : void BasicBlockEntry::ExeMainEntryHook(ExeMainEntryFrame* entry_frame) {
455 E : ScopedLastErrorKeeper scoped_last_error_keeper;
456 E : DCHECK(entry_frame != NULL);
457 E : Instance()->OnProcessAttach(entry_frame->module_data);
458 E : }
459 :
460 E : void BasicBlockEntry::RegisterModule(const void* addr) {
461 E : DCHECK(addr != NULL);
462 :
463 : // Allocate a segment for the module information.
464 E : trace::client::TraceFileSegment module_info_segment;
465 E : CHECK(session_.AllocateBuffer(&module_info_segment));
466 :
467 : // Log the module. This is required in order to associate basic-block
468 : // frequency with a module and PDB file during post-processing.
469 E : HMODULE module = GetModuleForAddr(addr);
470 E : CHECK(module != NULL);
471 E : CHECK(agent::common::LogModule(module, &session_, &module_info_segment));
472 :
473 : // Commit the module information.
474 E : CHECK(session_.ReturnBuffer(&module_info_segment));
475 E : }
476 :
477 E : void BasicBlockEntry::OnProcessAttach(IndexedFrequencyData* module_data) {
478 E : DCHECK(module_data != NULL);
479 :
480 : // Exit if the magic number does not match.
481 E : CHECK_EQ(::common::kBasicBlockEntryAgentId, module_data->agent_id);
482 :
483 : // Exit if the version does not match.
484 E : CHECK(DatatypeVersionIsValid(module_data->data_type, module_data->version));
485 :
486 : // We allow for this hook to be called multiple times. We expect the first
487 : // time to occur under the loader lock, so we don't need to worry about
488 : // concurrency for this check.
489 E : if (module_data->initialization_attempted)
490 E : return;
491 :
492 : // Flag the module as initialized.
493 E : module_data->initialization_attempted = 1U;
494 :
495 : // We expect this to be executed exactly once for each module.
496 E : CHECK_EQ(TLS_OUT_OF_INDEXES, module_data->tls_index);
497 E : module_data->tls_index = ::TlsAlloc();
498 E : CHECK_NE(TLS_OUT_OF_INDEXES, module_data->tls_index);
499 :
500 : // Register this module with the call_trace if the session is not disabled.
501 : // Note that we expect module_data to be statically defined within the
502 : // module of interest, so we can use its address to lookup the module.
503 E : if (!session_.IsDisabled())
504 E : RegisterModule(module_data);
505 E : }
506 :
507 E : void BasicBlockEntry::OnThreadDetach(IndexedFrequencyData* module_data) {
508 E : DCHECK(module_data != NULL);
509 E : DCHECK_EQ(1U, module_data->initialization_attempted);
510 E : DCHECK_NE(TLS_OUT_OF_INDEXES, module_data->tls_index);
511 :
512 E : ThreadState* state = ThreadState::Get(module_data->tls_index);
513 E : if (state != NULL)
514 E : thread_state_manager_.MarkForDeath(state);
515 E : }
516 :
517 : BasicBlockEntry::ThreadState* BasicBlockEntry::CreateThreadState(
518 E : IndexedFrequencyData* module_data) {
519 E : DCHECK(module_data != NULL);
520 E : CHECK_NE(IndexedFrequencyData::INVALID_DATA_TYPE, module_data->data_type);
521 :
522 : // Create the thread-local state for this thread. By default, just point the
523 : // counter array to the statically allocated fall-back area.
524 E : ThreadState* state = new ThreadState(this, module_data->frequency_data);
525 E : CHECK(state != NULL);
526 :
527 : // Associate the thread_state with the current thread.
528 E : state->Assign(module_data->tls_index);
529 :
530 : // Register the thread state with the thread state manager.
531 E : thread_state_manager_.Register(state);
532 :
533 : // If we're not actually tracing, then we're done.
534 E : if (session_.IsDisabled())
535 E : return state;
536 :
537 : // Nothing to allocate? We're done!
538 E : if (module_data->num_entries == 0) {
539 i : LOG(WARNING) << "Module contains no instrumented basic blocks, not "
540 : << "allocating basic-block trace data segment.";
541 i : return state;
542 : }
543 :
544 : // Determine the size of the basic block frequency table.
545 E : size_t data_size = module_data->num_entries * sizeof(uint32);
546 :
547 : // Determine the size of the basic block frequency record.
548 E : size_t record_size = sizeof(TraceIndexedFrequencyData) + data_size - 1;
549 :
550 : // Determine the size of the buffer we need. We need room for the basic block
551 : // frequency struct plus a single RecordPrefix header.
552 E : size_t segment_size = sizeof(RecordPrefix) + record_size;
553 :
554 : // Allocate the actual segment for the basic block entry data.
555 E : CHECK(session_.AllocateBuffer(segment_size, state->segment()));
556 :
557 : // Ensure it's big enough to allocate the basic-block frequency data
558 : // we want. This automatically accounts for the RecordPrefix overhead.
559 E : CHECK(state->segment()->CanAllocate(record_size));
560 :
561 : // Allocate the basic-block frequency data. We will leave this allocated and
562 : // let it get flushed during tear-down of the call-trace client.
563 : TraceIndexedFrequencyData* trace_data =
564 : reinterpret_cast<TraceIndexedFrequencyData*>(
565 : state->segment()->AllocateTraceRecordImpl(TRACE_INDEXED_FREQUENCY,
566 E : record_size));
567 E : DCHECK(trace_data != NULL);
568 :
569 : // Initialize the basic block frequency data struct.
570 E : HMODULE module = GetModuleForAddr(module_data);
571 E : CHECK(module != NULL);
572 E : const base::win::PEImage image(module);
573 E : const IMAGE_NT_HEADERS* nt_headers = image.GetNTHeaders();
574 E : trace_data->data_type = module_data->data_type;
575 E : trace_data->module_base_addr = reinterpret_cast<ModuleAddr>(image.module());
576 E : trace_data->module_base_size = nt_headers->OptionalHeader.SizeOfImage;
577 E : trace_data->module_checksum = nt_headers->OptionalHeader.CheckSum;
578 E : trace_data->module_time_date_stamp = nt_headers->FileHeader.TimeDateStamp;
579 E : trace_data->frequency_size = sizeof(uint32);
580 E : trace_data->num_entries = module_data->num_entries;
581 :
582 : // Hook up the newly allocated buffer to the call-trace instrumentation.
583 E : state->set_frequency_data(trace_data->frequency_data);
584 :
585 E : return state;
586 E : }
587 :
588 : } // namespace basic_block_entry
589 : } // namespace agent
|