OLD | NEW |
---|---|
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 // The client dump tool for libheap_profiler. It attaches to a process (given | 5 // The client dump tool for libheap_profiler. It attaches to a process (given |
6 // its pid) and dumps all the libheap_profiler tracking information in JSON. | 6 // its pid) and dumps all the libheap_profiler tracking information in JSON. |
7 // The target process is frozen (SIGSTOP) while dumping, unless the -n opt. | |
8 // is passed (in which case the caller is responsible for un/freezing). | |
9 // The JSON output looks like this: | 7 // The JSON output looks like this: |
10 // { | 8 // { |
11 // "total_allocated": 908748493, # Total bytes allocated and not freed. | 9 // "total_allocated": 908748493, # Total bytes allocated and not freed. |
12 // "num_allocs": 37542, # Number of allocations. | 10 // "num_allocs": 37542, # Number of allocations. |
13 // "num_stacks": 3723, # Number of allocation call-sites. | 11 // "num_stacks": 3723, # Number of allocation call-sites. |
14 // "allocs": # Optional. Printed only with the -x arg. | 12 // "allocs": # Optional. Printed only with the -x arg. |
15 // { | 13 // { |
16 // "beef1234": {"l": 17, "f": 1, "s": "1a"}, | 14 // "beef1234": {"l": 17, "f": 1, "s": "1a"}, |
17 // ^ ^ ^ ^ Index of the corresponding entry in the | 15 // ^ ^ ^ ^ Index of the corresponding entry in the |
18 // | | | next "stacks" section. Essentially a ref | 16 // | | | next "stacks" section. Essentially a ref |
19 // | | | to the call site that created the alloc. | 17 // | | | to the call site that created the alloc. |
20 // | | | | 18 // | | | |
21 // | | +-------> Flags (last arg of heap_profiler_alloc). | 19 // | | +-------> Flags (last arg of heap_profiler_alloc). |
22 // | +----------------> Length of the Alloc. | 20 // | +----------------> Length of the Alloc. |
23 // +-----------------------------> Start address of the Alloc (hex). | 21 // +-----------------------------> Start address of the Alloc (hex). |
24 // }, | 22 // }, |
25 // "stacks": | 23 // "stacks": |
26 // { | 24 // { |
27 // "1a": {"l": 17, "f": [1074792772, 1100849864, 1100850688, ...]}, | 25 // "1a": {"l": 17, "f": [1074792772, 1100849864, 1100850688, ...]}, |
28 // ^ ^ ^ | 26 // ^ ^ ^ |
29 // | | +-----> Stack frames (absolute virtual addresses). | 27 // | | +-----> Stack frames (absolute virtual addresses). |
30 // | +--------------> Bytes allocated and not freed by the call site. | 28 // | +--------------> Bytes allocated and not freed by the call site. |
31 // +---------------------> Index of the entry (as for "allocs" xref). | 29 // +---------------------> Index of the entry (as for "allocs" xref). |
32 // Indexes are hex and might not be monotonic. | 30 // Indexes are hex and might not be monotonic. |
33 | 31 |
32 #include <errno.h> | |
34 #include <fcntl.h> | 33 #include <fcntl.h> |
35 #include <inttypes.h> | 34 #include <inttypes.h> |
36 #include <signal.h> | |
37 #include <stdbool.h> | 35 #include <stdbool.h> |
38 #include <stdio.h> | 36 #include <stdio.h> |
39 #include <stdlib.h> | 37 #include <stdlib.h> |
40 #include <string.h> | 38 #include <string.h> |
41 #include <time.h> | 39 #include <time.h> |
42 #include <unistd.h> | 40 #include <unistd.h> |
41 #include <sys/ptrace.h> | |
43 #include <sys/stat.h> | 42 #include <sys/stat.h> |
44 | 43 |
45 #include "tools/android/heap_profiler/heap_profiler.h" | 44 #include "tools/android/heap_profiler/heap_profiler.h" |
46 | 45 |
47 | 46 |
48 static void lseek_abs(int fd, size_t off); | 47 static void lseek_abs(int fd, size_t off); |
49 static void read_proc_cmdline(char* cmdline, int size); | 48 static void read_proc_cmdline(char* cmdline, int size); |
49 static ssize_t read_safe(int fd, void* buf, size_t count); | |
50 | 50 |
51 static int pid; | 51 static int pid; |
52 | 52 |
53 | 53 |
54 static int dump_process_heap( | 54 static int dump_process_heap( |
55 int mem_fd, | 55 int mem_fd, |
56 FILE* fmaps, | 56 FILE* fmaps, |
57 bool dump_also_allocs, | 57 bool dump_also_allocs, |
58 bool pedantic, // Enable pedantic consistency checks on memory counters. | |
58 char* comment) { | 59 char* comment) { |
59 HeapStats stats; | 60 HeapStats stats; |
60 time_t tm; | 61 time_t tm; |
61 char cmdline[512]; | 62 char cmdline[512]; |
62 | 63 |
63 tm = time(NULL); | 64 tm = time(NULL); |
64 read_proc_cmdline(cmdline, sizeof(cmdline)); | 65 read_proc_cmdline(cmdline, sizeof(cmdline)); |
65 | 66 |
66 // Look for the mmap which contains the HeapStats in the target process vmem. | 67 // Look for the mmap which contains the HeapStats in the target process vmem. |
67 // On Linux/Android, the libheap_profiler mmaps explicitly /dev/zero. The | 68 // On Linux/Android, the libheap_profiler mmaps explicitly /dev/zero. The |
68 // region furthermore starts with a magic marker to disambiguate. | 69 // region furthermore starts with a magic marker to disambiguate. |
69 bool stats_mmap_found = false; | 70 bool stats_mmap_found = false; |
70 for (;;) { | 71 for (;;) { |
71 char line[1024]; | 72 char line[1024]; |
72 if (fgets(line, sizeof(line), fmaps) == NULL) | 73 if (fgets(line, sizeof(line), fmaps) == NULL) |
73 break; | 74 break; |
74 | 75 |
75 uintptr_t start; | 76 uintptr_t start; |
76 uintptr_t end; | 77 uintptr_t end; |
77 char map_file[32]; | 78 char map_file[32]; |
78 int ret = sscanf(line, "%"SCNxPTR"-%"SCNxPTR" rw-p %*s %*s %*s %31s", | 79 int ret = sscanf(line, "%"SCNxPTR"-%"SCNxPTR" rw-p %*s %*s %*s %31s", |
79 &start, &end, map_file); | 80 &start, &end, map_file); |
80 const size_t size = end - start + 1; | 81 const size_t size = end - start + 1; |
81 if (ret != 3 || strcmp(map_file, "/dev/zero") != 0 || size < sizeof(stats)) | 82 if (ret != 3 || strcmp(map_file, "/dev/zero") != 0 || size < sizeof(stats)) |
82 continue; | 83 continue; |
83 | 84 |
84 // The mmap looks promising. Let's check for the magic marker. | 85 // The mmap looks promising. Let's check for the magic marker. |
85 lseek_abs(mem_fd, start); | 86 lseek_abs(mem_fd, start); |
86 if (read(mem_fd, &stats, sizeof(stats)) < sizeof(stats)) | 87 ssize_t rsize = read_safe(mem_fd, &stats, sizeof(stats)); |
88 | |
89 if (rsize == -1) { | |
90 perror("read"); | |
91 return -1; | |
92 } | |
93 | |
94 if (rsize < sizeof(stats)) | |
87 continue; | 95 continue; |
88 | 96 |
89 if (stats.magic_start == HEAP_PROFILER_MAGIC_MARKER) { | 97 if (stats.magic_start == HEAP_PROFILER_MAGIC_MARKER) { |
90 stats_mmap_found = true; | 98 stats_mmap_found = true; |
91 break; | 99 break; |
92 } | 100 } |
93 } | 101 } |
94 | 102 |
95 if (!stats_mmap_found) { | 103 if (!stats_mmap_found) { |
96 fprintf(stderr, "Could not find the HeapStats area. " | 104 fprintf(stderr, "Could not find the HeapStats area. " |
(...skipping 16 matching lines...) Expand all Loading... | |
113 size_t dbg_counted_total_alloc_bytes = 0; | 121 size_t dbg_counted_total_alloc_bytes = 0; |
114 bool prepend_trailing_comma = false; // JSON syntax, I hate you. | 122 bool prepend_trailing_comma = false; // JSON syntax, I hate you. |
115 uint32_t i; | 123 uint32_t i; |
116 | 124 |
117 // Dump the optional allocation table. | 125 // Dump the optional allocation table. |
118 if (dump_also_allocs) { | 126 if (dump_also_allocs) { |
119 printf(" \"allocs\": {"); | 127 printf(" \"allocs\": {"); |
120 lseek_abs(mem_fd, (uintptr_t) stats.allocs); | 128 lseek_abs(mem_fd, (uintptr_t) stats.allocs); |
121 for (i = 0; i < stats.max_allocs; ++i) { | 129 for (i = 0; i < stats.max_allocs; ++i) { |
122 Alloc alloc; | 130 Alloc alloc; |
123 if (read(mem_fd, &alloc, sizeof(alloc)) != sizeof(alloc)) { | 131 if (read_safe(mem_fd, &alloc, sizeof(alloc)) != sizeof(alloc)) { |
124 fprintf(stderr, "ERROR: cannot read allocation table\n"); | 132 fprintf(stderr, "ERROR: cannot read allocation table\n"); |
125 perror("read"); | 133 perror("read"); |
126 return -1; | 134 return -1; |
127 } | 135 } |
128 | 136 |
129 // Skip empty (i.e. freed) entries. | 137 // Skip empty (i.e. freed) entries. |
130 if (alloc.start == 0 && alloc.end == 0) | 138 if (alloc.start == 0 && alloc.end == 0) |
131 continue; | 139 continue; |
132 | 140 |
133 if (alloc.end < alloc.start) { | 141 if (alloc.end < alloc.start) { |
134 fprintf(stderr, "ERROR: found inconsistent alloc.\n"); | 142 fprintf(stderr, "ERROR: found inconsistent alloc.\n"); |
135 return -1; | 143 return -1; |
136 } | 144 } |
137 | 145 |
138 size_t alloc_size = alloc.end - alloc.start + 1; | 146 size_t alloc_size = alloc.end - alloc.start + 1; |
139 size_t stack_idx = ( | 147 size_t stack_idx = ( |
140 (uintptr_t) alloc.st - (uintptr_t) stats.stack_traces) / | 148 (uintptr_t) alloc.st - (uintptr_t) stats.stack_traces) / |
141 sizeof(StacktraceEntry); | 149 sizeof(StacktraceEntry); |
142 dbg_counted_total_alloc_bytes += alloc_size; | 150 dbg_counted_total_alloc_bytes += alloc_size; |
143 ++dbg_counted_allocs; | 151 ++dbg_counted_allocs; |
144 | 152 |
145 if (prepend_trailing_comma) | 153 if (prepend_trailing_comma) |
146 printf(","); | 154 printf(","); |
147 prepend_trailing_comma = true; | 155 prepend_trailing_comma = true; |
148 printf("\"%"PRIxPTR"\": {\"l\": %zu, \"f\": %"PRIu32", \"s\": \"%zx\"}", | 156 printf("\"%"PRIxPTR"\": {\"l\": %zu, \"f\": %"PRIu32", \"s\": \"%zx\"}", |
149 alloc.start, alloc_size, alloc.flags, stack_idx); | 157 alloc.start, alloc_size, alloc.flags, stack_idx); |
150 } | 158 } |
151 printf("},\n"); | 159 printf("},\n"); |
152 | 160 |
153 if (dbg_counted_allocs != stats.num_allocs) { | 161 if (pedantic && dbg_counted_allocs != stats.num_allocs) { |
154 fprintf(stderr, | 162 fprintf(stderr, |
155 "ERROR: inconsistent alloc count (%"PRIu32" vs %"PRIu32").\n", | 163 "ERROR: inconsistent alloc count (%"PRIu32" vs %"PRIu32").\n", |
156 dbg_counted_allocs, stats.num_allocs); | 164 dbg_counted_allocs, stats.num_allocs); |
157 return -1; | 165 return -1; |
158 } | 166 } |
159 | 167 |
160 if (dbg_counted_total_alloc_bytes != stats.total_alloc_bytes) { | 168 if (pedantic && dbg_counted_total_alloc_bytes != stats.total_alloc_bytes) { |
161 fprintf(stderr, "ERROR: inconsistent alloc totals (%zu vs %zu).\n", | 169 fprintf(stderr, "ERROR: inconsistent alloc totals (%zu vs %zu).\n", |
162 dbg_counted_total_alloc_bytes, stats.total_alloc_bytes); | 170 dbg_counted_total_alloc_bytes, stats.total_alloc_bytes); |
163 return -1; | 171 return -1; |
164 } | 172 } |
165 } | 173 } |
166 | 174 |
167 // Dump the distinct stack traces. | 175 // Dump the distinct stack traces. |
168 printf(" \"stacks\": {"); | 176 printf(" \"stacks\": {"); |
169 prepend_trailing_comma = false; | 177 prepend_trailing_comma = false; |
170 dbg_counted_total_alloc_bytes = 0; | 178 dbg_counted_total_alloc_bytes = 0; |
171 lseek_abs(mem_fd, (uintptr_t) stats.stack_traces); | 179 lseek_abs(mem_fd, (uintptr_t) stats.stack_traces); |
172 for (i = 0; i < stats.max_stack_traces; ++i) { | 180 for (i = 0; i < stats.max_stack_traces; ++i) { |
173 StacktraceEntry st; | 181 StacktraceEntry st; |
174 if (read(mem_fd, &st, sizeof(st)) != sizeof(st)) { | 182 if (read_safe(mem_fd, &st, sizeof(st)) != sizeof(st)) { |
175 fprintf(stderr, "ERROR: cannot read stack trace table\n"); | 183 fprintf(stderr, "ERROR: cannot read stack trace table\n"); |
176 perror("read"); | 184 perror("read"); |
177 return -1; | 185 return -1; |
178 } | 186 } |
179 | 187 |
180 // Skip empty (i.e. freed) entries. | 188 // Skip empty (i.e. freed) entries. |
181 if (st.alloc_bytes == 0) | 189 if (st.alloc_bytes == 0) |
182 continue; | 190 continue; |
183 | 191 |
184 dbg_counted_total_alloc_bytes += st.alloc_bytes; | 192 dbg_counted_total_alloc_bytes += st.alloc_bytes; |
185 | 193 |
186 if (prepend_trailing_comma) | 194 if (prepend_trailing_comma) |
187 printf(","); | 195 printf(","); |
188 prepend_trailing_comma = true; | 196 prepend_trailing_comma = true; |
189 | 197 |
190 printf("\"%"PRIx32"\":{\"l\": %zu, \"f\": [", i, st.alloc_bytes); | 198 printf("\"%"PRIx32"\":{\"l\": %zu, \"f\": [", i, st.alloc_bytes); |
191 size_t n = 0; | 199 size_t n = 0; |
192 for (;;) { | 200 for (;;) { |
193 printf("%" PRIuPTR, st.frames[n]); | 201 printf("%" PRIuPTR, st.frames[n]); |
194 ++n; | 202 ++n; |
195 if (n == HEAP_PROFILER_MAX_DEPTH || st.frames[n] == 0) | 203 if (n == HEAP_PROFILER_MAX_DEPTH || st.frames[n] == 0) |
196 break; | 204 break; |
197 else | 205 else |
198 printf(","); | 206 printf(","); |
199 } | 207 } |
200 printf("]}"); | 208 printf("]}"); |
201 } | 209 } |
202 printf("}\n}\n"); | 210 printf("}\n}\n"); |
203 | 211 |
204 if (dbg_counted_total_alloc_bytes != stats.total_alloc_bytes) { | 212 if (pedantic && dbg_counted_total_alloc_bytes != stats.total_alloc_bytes) { |
205 fprintf(stderr, "ERROR: inconsistent stacks totals (%zu vs %zu).\n", | 213 fprintf(stderr, "ERROR: inconsistent stacks totals (%zu vs %zu).\n", |
206 dbg_counted_total_alloc_bytes, stats.total_alloc_bytes); | 214 dbg_counted_total_alloc_bytes, stats.total_alloc_bytes); |
207 return -1; | 215 return -1; |
208 } | 216 } |
209 | 217 |
210 fflush(stdout); | 218 fflush(stdout); |
211 return 0; | 219 return 0; |
212 } | 220 } |
213 | 221 |
214 // If the dump is interrupted, resume the target process before exiting. | |
215 static void exit_handler() { | |
216 kill(pid, SIGCONT); | |
217 waitpid(pid, NULL, 0); | |
218 exit(-1); | |
219 } | |
220 | |
221 static bool freeze_process() { | |
222 if (kill(pid, SIGSTOP) != 0) { | |
223 fprintf(stderr, "Could not freeze the target process.\n"); | |
224 perror("kill"); | |
225 return false; | |
226 } | |
227 | |
228 signal(SIGPIPE, exit_handler); | |
229 signal(SIGINT, exit_handler); | |
230 return true; | |
231 } | |
232 | |
233 // Unfortunately lseek takes a *signed* offset, which is unsuitable for large | 222 // Unfortunately lseek takes a *signed* offset, which is unsuitable for large |
234 // files like /proc/X/mem on 64-bit. | 223 // files like /proc/X/mem on 64-bit. |
235 static void lseek_abs(int fd, size_t off) { | 224 static void lseek_abs(int fd, size_t off) { |
236 #define OFF_T_MAX ((off_t) ~(((uint64_t) 1) << (8 * sizeof(off_t) - 1))) | 225 #define OFF_T_MAX ((off_t) ~(((uint64_t) 1) << (8 * sizeof(off_t) - 1))) |
237 if (off <= OFF_T_MAX) { | 226 if (off <= OFF_T_MAX) { |
238 lseek(fd, (off_t) off, SEEK_SET); | 227 lseek(fd, (off_t) off, SEEK_SET); |
239 return; | 228 return; |
240 } | 229 } |
241 lseek(fd, (off_t) OFF_T_MAX, SEEK_SET); | 230 lseek(fd, (off_t) OFF_T_MAX, SEEK_SET); |
242 lseek(fd, (off_t) (off - OFF_T_MAX), SEEK_CUR); | 231 lseek(fd, (off_t) (off - OFF_T_MAX), SEEK_CUR); |
243 } | 232 } |
244 | 233 |
234 static ssize_t read_safe(int fd, void* buf, size_t count) { | |
235 ssize_t res; | |
236 do { | |
237 res = read(fd, buf, count); | |
238 } while(res == 1 && errno == EINTR); | |
pasko
2014/09/10 15:25:23
s/1/-1/
And it still can be a short read. Take a
Primiano Tucci (use gerrit)
2014/09/10 15:42:16
That is veeeeeeery unlikely to happen when reading
pasko
2014/09/10 16:09:27
yes, it's unlikely, and your tool is unlikely to b
| |
239 return res; | |
240 } | |
241 | |
245 static int open_proc_mem_fd() { | 242 static int open_proc_mem_fd() { |
246 char path[64]; | 243 char path[64]; |
247 snprintf(path, sizeof(path), "/proc/%d/mem", pid); | 244 snprintf(path, sizeof(path), "/proc/%d/mem", pid); |
248 int mem_fd = open(path, O_RDONLY); | 245 int mem_fd = open(path, O_RDONLY); |
249 if (mem_fd < 0) { | 246 if (mem_fd < 0) { |
250 fprintf(stderr, "Could not attach to target process virtual memory.\n"); | 247 fprintf(stderr, "Could not attach to target process virtual memory.\n"); |
251 perror("open"); | 248 perror("open"); |
252 } | 249 } |
253 return mem_fd; | 250 return mem_fd; |
254 } | 251 } |
(...skipping 12 matching lines...) Expand all Loading... | |
267 static void read_proc_cmdline(char* cmdline, int size) { | 264 static void read_proc_cmdline(char* cmdline, int size) { |
268 char path[64]; | 265 char path[64]; |
269 snprintf(path, sizeof(path), "/proc/%d/cmdline", pid); | 266 snprintf(path, sizeof(path), "/proc/%d/cmdline", pid); |
270 int cmdline_fd = open(path, O_RDONLY); | 267 int cmdline_fd = open(path, O_RDONLY); |
271 if (cmdline_fd < 0) { | 268 if (cmdline_fd < 0) { |
272 fprintf(stderr, "Could not open %s.\n", path); | 269 fprintf(stderr, "Could not open %s.\n", path); |
273 perror("open"); | 270 perror("open"); |
274 cmdline[0] = '\0'; | 271 cmdline[0] = '\0'; |
275 return; | 272 return; |
276 } | 273 } |
277 int length = read(cmdline_fd, cmdline, size); | 274 int length = read_safe(cmdline_fd, cmdline, size); |
278 if (length < 0) { | 275 if (length < 0) { |
279 fprintf(stderr, "Could not read %s.\n", path); | 276 fprintf(stderr, "Could not read %s.\n", path); |
280 perror("read"); | 277 perror("read"); |
281 length = 0; | 278 length = 0; |
282 } | 279 } |
283 close(cmdline_fd); | 280 close(cmdline_fd); |
284 cmdline[length] = '\0'; | 281 cmdline[length] = '\0'; |
285 } | 282 } |
286 | 283 |
287 int main(int argc, char** argv) { | 284 int main(int argc, char** argv) { |
288 char c; | 285 char c; |
289 int ret = 0; | 286 int ret = 0; |
290 bool should_freeze_process = true; | |
291 bool dump_also_allocs = false; | 287 bool dump_also_allocs = false; |
288 bool pedantic = true; | |
292 char comment[1024] = { '\0' }; | 289 char comment[1024] = { '\0' }; |
293 | 290 |
294 while (((c = getopt(argc, argv, "nxc:")) & 0x80) == 0) { | 291 while (((c = getopt(argc, argv, "xnc:")) & 0x80) == 0) { |
295 switch (c) { | 292 switch (c) { |
296 case 'n': | |
297 should_freeze_process = false; | |
298 break; | |
299 case 'x': | 293 case 'x': |
300 dump_also_allocs = true; | 294 dump_also_allocs = true; |
301 break; | 295 break; |
296 case 'n': | |
297 pedantic = false; | |
298 break; | |
302 case 'c': | 299 case 'c': |
303 strlcpy(comment, optarg, sizeof(comment)); | 300 strlcpy(comment, optarg, sizeof(comment)); |
304 break; | 301 break; |
305 } | 302 } |
306 } | 303 } |
307 | 304 |
308 if (optind >= argc) { | 305 if (optind >= argc) { |
309 printf("Usage: %s [-n] [-x] [-c comment] pid\n", argv[0]); | 306 printf("Usage: %s [-n] [-x] [-c comment] pid\n" |
307 " -n: Skip pedantic checks on dump consistency.\n" | |
308 " -x: Extended dump, includes individual allocations.\n" | |
309 " -c: Appends the given comment to the JSON dump.\n", | |
310 argv[0]); | |
310 return -1; | 311 return -1; |
311 } | 312 } |
312 | 313 |
313 pid = atoi(argv[optind]); | 314 pid = atoi(argv[optind]); |
314 | 315 |
315 if (should_freeze_process && !freeze_process()) | 316 if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1) { |
317 perror("ptrace"); | |
316 return -1; | 318 return -1; |
319 } | |
317 | 320 |
318 // Wait for the process to actually freeze. | 321 // Wait for the process to actually freeze. |
319 waitpid(pid, NULL, 0); | 322 waitpid(pid, NULL, 0); |
320 | 323 |
321 int mem_fd = open_proc_mem_fd(); | 324 int mem_fd = open_proc_mem_fd(); |
322 if (mem_fd < 0) | 325 if (mem_fd < 0) |
323 ret = -1; | 326 ret = -1; |
324 | 327 |
325 FILE* fmaps = open_proc_maps(); | 328 FILE* fmaps = open_proc_maps(); |
326 if (fmaps == NULL) | 329 if (fmaps == NULL) |
327 ret = -1; | 330 ret = -1; |
328 | 331 |
329 if (ret == 0) | 332 if (ret == 0) |
330 ret = dump_process_heap(mem_fd, fmaps, dump_also_allocs, comment); | 333 ret = dump_process_heap(mem_fd, fmaps, dump_also_allocs, pedantic, comment); |
331 | 334 |
332 if (should_freeze_process) | 335 ptrace(PTRACE_DETACH, pid, NULL, NULL); |
333 kill(pid, SIGCONT); | |
334 | 336 |
335 // Cleanup. | 337 // Cleanup. |
336 fflush(stdout); | 338 fflush(stdout); |
337 close(mem_fd); | 339 close(mem_fd); |
338 fclose(fmaps); | 340 fclose(fmaps); |
339 return ret; | 341 return ret; |
340 } | 342 } |
OLD | NEW |