通过泄露的内存数据内容定位相关代码
尽管笔者使用了Lua脚本做一些系统层的嵌入软件开发的工作,也使用Rust编程语言实现了一些嵌入式应用层的功能,但基于C
语言的历史代码在工作中仍占据主要的部分。这些C
语言代码存在一些内存泄露问题,但这并不完全是开发者的“过错”:C/C++
编程语言本质上是内存、线程不安全的;此外也与研发管理(如代码质量审核、软件测试等)相关——正因如此,笔者比较倾向于在工作中使用非C/C++
编程语言做软件开发。受制于嵌入式设备的有限资源,以及可供跟踪、分析内存泄露问题的调试环境,笔者很难使用一些高级的内存调试工具或库,如Valgrind、dmalloc等——这促使笔者不断尝试其他方法。
内存泄露的重要特征是,一个应用在长时间运行时,会不断地消耗内存。笔者基于这个特征,每隔一段时间(例如10分钟)记录一次应用的内存布局(进程的地址空间,通过记录/proc/XXXXX/maps
文件),之后选择两个记录的内存布局之间的差异,通过gdb工具将该增长的内存读出到文件,那么该文件的主要内容即为泄露的内存数据。最后结合代码,可以一步步确定泄露的内存数据可能是哪部分代码产生的,从而定位到内存泄露的问题。去年笔者使用该方法解决了一个内存泄露问题;最近笔者再次使用该方法解决了多个内存泄露的软件缺陷,并简单地编写了两个用于调试的Lua
脚本(工作中开发的脚本不便提供,笔者重新实现了二者的功能以便演示),因此将该方法分享出来。
周期保存某个应用的内存布局文件/proc/XXXXX/maps
文件
周期性地保存一个或多个应用的maps
文件,是该方法的第一步工作。这一步的功能最为简单,使用shell
脚本即可实现。为了方便增加监控的应用,笔者编写了一个mapsnap
的应用,可通过配置文件/tmp/mapsnap.json
指定监视的应用名称、保存周期、maps
文件保存路径等。例如,笔者在Ubuntu
系统下的配置文件/tmp/mapsnap.json
内容为:
{
"snap_interval": 600,
"store_root": "/root/app-maps",
"app_list": [
"ibus-daemon",
"update-notifier",
"wpa_supplicant",
"bluetoothd",
"memleak"
]
}
其中,memleak
为笔者编写的调试应用,它存在一个内存泄露问题。执行该应用会每600秒抓取应用的maps
文件;其输出结果如下:
root@ubuntu:~# ./mapsnap
Maps dumped for PID 1272 => /root/app-maps/bluetoothd/pid1272-20230701-213103.maps
Maps dumped for PID 1326 => /root/app-maps/wpa_supplicant/pid1326-20230701-213103.maps
Maps dumped for PID 2725 => /root/app-maps/ibus-daemon/pid2725-20230701-213103.maps
Maps dumped for PID 3229 => /root/app-maps/update-notifier/pid3229-20230701-213103.maps
Maps dumped for PID 3794 => /root/app-maps/memleak/pid3794-20230701-213103.maps
-----------------------------------------------------------
mapsnap
的源码会在本文末尾列出。
使用gdb
调试器抓取泄露的内存
首先,开发者需要评估某个应用的内存泄露速率以便抓取到可靠的泄露内存。因笔者编写的memleak
应用的内存泄露速度不高,隔了一夜后,由mapsnap
应用为memleak
抓取的maps
文件如下(为方便展示,删除了部分maps
文件):
root@ubuntu:~/app-maps/memleak# md5sum *.maps
d7ade7eaf733ab1de66d717ce69394ca pid4854-20230701-223455.maps
d7ade7eaf733ab1de66d717ce69394ca pid4854-20230701-223755.maps
19863c978603f3906bc78198611a97cc pid4854-20230701-223855.maps
19863c978603f3906bc78198611a97cc pid4854-20230701-223955.maps
19863c978603f3906bc78198611a97cc pid4854-20230701-224555.maps
79beee787c394d3fbd5e32992d24f288 pid4854-20230701-224655.maps
79beee787c394d3fbd5e32992d24f288 pid4854-20230701-224909.maps
4fdd499d9797a5f60213ab8b23413b8e pid4854-20230701-225909.maps
a70bb56dd0b2fef5f203abf1ca9a98b9 pid4854-20230701-230909.maps
c04f662cb3a1c498a499fcf9ec9a718d pid4854-20230701-231909.maps
c04da80980341215f9e83a4095561de7 pid4854-20230701-232909.maps
930a0e14150716cae93387267be5f909 pid4854-20230701-233909.maps
ebb609259d48bea876998002ac9305e8 pid4854-20230701-234909.maps
aa68ae943632789961b37eaa71f2c723 pid4854-20230701-235909.maps
74db1c29510272d9c11a5f73a817a02d pid4854-20230702-000909.maps
fd2fb623c23ecf06d056794e31959314 pid4854-20230702-020909.maps
e02778ddeb69e51a89bef6b39d638d0e pid4854-20230702-021909.maps
75b57e05e33cfe62d92d959aab1bcb02 pid4854-20230702-022909.maps
dfbe68c56e8eabfafedb9df32d29680c pid4854-20230702-023909.maps
7d9fb7705a2b944245bf1286a7287636 pid4854-20230702-024909.maps
0c88ef50afe37ffd58af25cc151e2b66 pid4854-20230702-025909.maps
ab8453a304faacec628ef2833827265e pid4854-20230702-030909.maps
bef9e89683a39d7b4777364c8e38869f pid4854-20230702-055909.maps
aa6df651e40bf97637ec6f7197366e9b pid4854-20230702-062909.maps
为什么基本上每一个maps
文件都不一样?因为memleak
应用的内存布局确实在“不断变化”,确切地说,是这个应用存在内存泄露的问题。对比之下,ibus-daemon
应用的内存布局则比较稳定(删除了部分输出内容):
root@ubuntu:~/app-maps/ibus-daemon# md5sum *
413a56eebcc7b6301f0f344fc6bec995 pid2708-20230701-223455.maps
413a56eebcc7b6301f0f344fc6bec995 pid2708-20230701-223555.maps
413a56eebcc7b6301f0f344fc6bec995 pid2708-20230701-223655.maps
413a56eebcc7b6301f0f344fc6bec995 pid2708-20230701-223755.maps
413a56eebcc7b6301f0f344fc6bec995 pid2708-20230701-224755.maps
413a56eebcc7b6301f0f344fc6bec995 pid2708-20230702-060909.maps
413a56eebcc7b6301f0f344fc6bec995 pid2708-20230702-061909.maps
413a56eebcc7b6301f0f344fc6bec995 pid2708-20230702-062909.maps
413a56eebcc7b6301f0f344fc6bec995 pid2708-20230702-063909.maps
接下来是比较重要的一步,即评估哪一段时间内不同的maps
文件中增长的内存大概率是泄露的内存。通常来讲,应用刚启动的一段时间,需要分配的内存尚未达到一种“动态平衡”,不同的maps
文件对比增长的内存并不以泄露的内存占据绝大部分;所以要避免对比应用刚启动不久抓取到的maps
文件。另外,也不应该选取抓取时间靠后的maps
文件,而应该是几十分钟前、甚至是几个小时、十几个小时前的,只有这样,其对比增长的内存区域中的数据才能够**“沉淀”**为大部分内容是泄露的数据。因此笔者对比的两个maps
文件是凌辰两点左右的:
root@ubuntu:~/app-maps/memleak# diff -u pid4854-20230702-021909.maps pid4854-20230702-023909.maps
--- pid4854-20230702-021909.maps 2023-06-27 02:19:09.784199061 +0800
+++ pid4854-20230702-023909.maps 2023-06-27 02:39:09.786238924 +0800
@@ -4,7 +4,7 @@
5570f94bb000-5570f94bc000 r--p 00002000 08:05 14445958 /home/yejq/program/blogs/20230624-memory-leak/memleak
5570f94bc000-5570f94bd000 rw-p 00003000 08:05 14445958 /home/yejq/program/blogs/20230624-memory-leak/memleak
5570f94bd000-5570f94be000 rw-p 00000000 00:00 0
-5570fa4f8000-5570fa8ad000 rw-p 00000000 00:00 0 [heap]
+5570fa4f8000-5570fa90f000 rw-p 00000000 00:00 0 [heap]
7f6eb72bf000-7f6eb72c2000 rw-p 00000000 00:00 0
7f6eb72c2000-7f6eb72ea000 r--p 00000000 08:04 2763553 /usr/lib/x86_64-linux-gnu/libc.so.6
7f6eb72ea000-7f6eb747f000 r-xp 00028000 08:04 2763553 /usr/lib/x86_64-linux-gnu/libc.so.6
因笔者编写的memleak
是单线程,以上两个maps
文件对比的结果只有一处内存区域增长了。对于多线程的应用,可能会有多个内存区域增长。之后笔者使用gdbdump
命令,它会调用gdb
调试工具抓取正在运行的memleak
的一段内存数据保存到文件,该段内存即为这两个maps
文件对比增长的内存,即5570fa8ad000-5570fa90f000
:
root@ubuntu:~/app-maps/memleak# /root/gdbdump 4854 5570fa8ad000-5570fa90f000
gdb-dump PID: 4854
Dumping memory to file: pid4854-0x5570fa8ad000-0x5570fa90f000.bin ...
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
0x00007f6eb73a77fa in clock_nanosleep () from /lib/x86_64-linux-gnu/libc.so.6
[Inferior 1 (process 4854) detached]
这一步也可以手动操作完成:执行gdb
调试器,并绑定(attach
)到正在运行的进程,之后执行gdb
的dump binary memory
命令以将一段内存保存到文件。笔者编写该gdbdump
应用,是为了尽量避免干扰进程的运行(某些现场环境下不能长时间干扰进程的运行)。注意,以上操作生成了pid4854-0x5570fa8ad000-0x5570fa90f000.bin
二进制文件。之后,笔者使用hexedit
、xxd
等工具查看该文件,可以看到泄露的内存区域的数据大致是这样的:
root@ubuntu:~/app-maps/memleak# hexedit pid4854-0x5570fa8ad000-0x5570fa90f000.bin
root@ubuntu:~/app-maps/memleak# xxd -s 4096 -l 256 -c 16 pid4854-0x5570fa8ad000-0x5570fa90f000.bin
00001000: 8484 8484 8484 8484 8484 8484 8484 8484 ................
00001010: 8484 8484 8484 8484 8484 8484 8484 8483 ................
00001020: 8383 8383 8383 8397 b101 0000 0000 0000 ................
00001030: 8484 8484 8484 8484 8484 8484 8484 8484 ................
00001040: 8484 8484 8484 8484 8484 8484 8484 8484 ................
00001050: 8484 8484 8484 8484 8484 8484 8484 8484 ................
00001060: 8484 8484 8484 8484 8484 8484 8484 8484 ................
00001070: 8484 8484 8484 8484 8484 8484 8484 8484 ................
00001080: 8484 8484 8484 8484 8484 8484 8484 8484 ................
00001090: 8484 8484 8484 8484 8484 8484 8484 8484 ................
000010a0: 8484 8484 8484 8484 8484 8484 8484 8484 ................
000010b0: 8484 8484 8484 8484 8484 8484 8484 8484 ................
000010c0: 8484 8484 8484 8484 8484 8484 8484 8484 ................
000010d0: 8484 8484 8484 8484 8484 8484 8484 8484 ................
000010e0: 8484 8484 8484 8484 8484 8484 8484 8484 ................
000010f0: 8484 8484 8484 8484 8484 8484 8484 8484 ................
root@ubuntu:~/app-maps/memleak#
至此,我们可以得到:memleak调试应用的内存泄露很可能与0x84相关这一初步结论。在实际的调试过程中,笔者观察到的泄露内存要比该处复杂得多,而最有帮助的是在二制文件中找到的字符串。不过值得强调的是,很多时候抓取到的内存确实是泄露的内存片段,但其中的数据可能不全是泄露的数据,也可能是已被释放了的内存,再次被申请后泄露了,但其数据保持之前一次分配后写入的数据。实际工作中,分析该二进制文件还可能需要了解glibc
等C语言库对内存分配的管理,如malloc_chunk的结构体等。无论如何,该方法已向定位内存泄露相关的代码迈出了很重要的一步。
接下来,我们带着0x84
这一可疑数据,查看一下memleak
的源码,可以确定以上调试结果是有效的,也可以确定相关内存泄露的代码:
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#define LEAK_INDEX 0x83
#define MEM_ARRAY_SIZE 512
static int rand_fd;
static unsigned char * mem_array[MEM_ARRAY_SIZE];
static void realloc_memory(int iter)
{
int idx;
ssize_t rl1;
for (idx = 0; idx < MEM_ARRAY_SIZE; ++idx) {
size_t msize = 0;
unsigned char * memp;
rl1 = read(rand_fd, &msize, sizeof(msize));
if (rl1 != (ssize_t) sizeof(msize)) {
fprintf(stderr, "Error, failed to read random device: %s\n",
strerror(errno));
fflush(stderr);
break;
}
msize &= 0x200 - 0x1;
memp = (unsigned char *) malloc(msize);
if (memp == NULL) {
fprintf(stderr, "Error, System Out Of Memory: %zu\n", msize);
fflush(stderr);
break;
}
if (msize > 0) {
memset(memp, idx + 1, msize);
}
if (idx == LEAK_INDEX) {
fprintf(stdout, "Leaking memory... Iteration: %d\n", iter);
fflush(stdout);
} else if (mem_array[idx] != NULL)
free(mem_array[idx]);
mem_array[idx] = memp;
}
}
int main(int argc, char *argv[])
{
int idx;
rand_fd = open("/dev/urandom", O_RDWR | O_CLOEXEC);
if (rand_fd < 0) {
fprintf(stderr, "Error, failed to open random device: %s\n",
strerror(errno));
fflush(stderr);
return 1;
}
for (idx = 0; idx < MEM_ARRAY_SIZE; ++idx) {
mem_array[idx] = NULL;
}
idx = 0;
while (idx < 86400) {
realloc_memory(idx++);
sleep(1);
}
for (idx = 0; idx < MEM_ARRAY_SIZE; ++idx) {
if (mem_array[idx] != NULL) {
free(mem_array[idx]);
mem_array[idx] = NULL;
}
}
close(rand_fd);
rand_fd = -1;
fputs("Memory-leaking application will now exit.\n", stdout);
fflush(stdout);
return 0;
}
最后,需要强调的是,该方法可以协助定位大多数的内存泄露问题,但并不是全部。如果某段代码只调用malloc
分配内存,但不在分配的内存中写入数据,那本方法将很难有帮助。例如,以上代码中的:
memset(memp, idx + 1, msize);
是本方法有效的关键之处。笔者在实际工作中,就遇到了这种只分配内存但不写入数据的内存泄露问题,目前仍是一筹莫展(虽然有其他方法,但受限于调试环境,不便深入定位)。让人感到一丝欣慰的是,该方法已协助定位并解决了多个内存泄露问题。
相关代码
笔者在工作中实现的两个Lua
脚本,因太过浅陋不便展示。笔者在工作之外编写了mapsnap
、gdbdump
两个简单工具(坦诚地说,使用Lua
脚本实现得更快也更好),由Rust
语言编写,代码内容分别如下(不展示Cargo.toml
文件)。
代码mapsnap.rs
的内容如下:
use std::collections::HashSet;
use serde::Deserialize;
use chrono::{FixedOffset, DateTime, NaiveDateTime};
use libc::{timespec, clock_gettime, CLOCK_BOOTTIME, CLOCK_REALTIME};
struct Appid {
pid: u32,
name: String,
}
impl Appid {
fn new(p: u32, n: String) -> Self {
Self { pid: p, name: n }
}
fn snapshot(&self, pdir: &str) -> bool {
let mfile = format!("/proc/{}/maps", self.pid);
let maps = match std::fs::read_to_string(&mfile) {
Ok(content) => content,
Err(err) => {
eprintln!("Error, failed to read '{}': {}", mfile, err);
return false;
},
};
let appdir = format!("{}/{}", pdir, self.name);
let _ = std::fs::create_dir(&appdir); // create parent directory
let pfile = format!("{}/pid{}-{}.maps", appdir, self.pid, timestr());
let pfile = std::path::PathBuf::from(pfile);
if let Err(err) = std::fs::write(&pfile, maps.as_bytes()) {
eprintln!("Error, failed to write file '{}': {}", pfile.display(), err);
false
} else {
println!("Maps dumped for PID {} => {}", self.pid, pfile.display());
true
}
}
}
#[derive(Deserialize)]
struct Mapscfg {
#[serde(rename = "snap_interval")]
interval: u32,
#[serde(rename = "store_root")]
maproot: String,
#[serde(rename = "app_list")]
apps: Vec<String>,
#[serde(default, skip)]
appset: HashSet<String>,
}
impl Mapscfg {
fn is_valid(&self) -> bool {
if self.interval <= 0x5 || self.interval >= 0x418938 ||
self.maproot.is_empty() || self.apps.is_empty() {
return false;
}
let mroot = self.maproot.clone();
let mroot = std::path::PathBuf::from(mroot);
let _ = std::fs::create_dir_all(&mroot);
mroot.metadata().ok().map(|meta| {
let isdir = meta.is_dir();
if !isdir {
eprintln!("Error, store_root is not a directory: {}", mroot.display());
}
isdir
}).unwrap_or_else(|| {
eprintln!("Error, cannot find store_root: {}", mroot.display());
false
})
}
fn hash_app(&mut self) {
for app in &self.apps {
let appn = app.clone();
self.appset.insert(appn);
}
}
fn load_cfg(cfg: &str) -> Option<Self> {
let mcfg = cfg.to_string();
let mcfg = std::path::PathBuf::from(mcfg);
let msize: u64 = match mcfg.metadata() {
Ok(meta) if meta.is_file() => meta.len(),
_ => 0,
};
if msize == 0 || msize >= 0x100000 {
eprintln!("Error, invalid config-file-size for '{}': {}", cfg, msize);
return None;
}
let mbuf: String = match std::fs::read_to_string(&mcfg) {
Ok(buf) => buf,
Err(err) => {
eprintln!("Error, failed to read {} as UTF-8 string: {}", cfg, err);
return None;
},
};
let mut mapcfg: Self = match serde_json::from_str(mbuf.as_str()) {
Ok(mcfg) => mcfg,
Err(err) => {
eprintln!("Error, failed to decode '{}' as intended struct: {}", cfg, err);
return None;
},
};
if mapcfg.is_valid() { mapcfg.hash_app(); Some(mapcfg) } else { None }
}
}
// TODO: iterate a list of processes with `glob crate
// TODO: fetch application list with command-line utility, `pidof
fn fetch_applist(mcfg: &Mapscfg) -> Option<Vec<Appid>> {
let iterd = std::fs::read_dir("/proc").ok()?;
let apps: Vec<Appid> = iterd.filter_map(|pfile| {
// ignore iterator dentry error:
let dire = pfile.ok()?;
// check the file type:
let meta = dire.metadata().ok()?;
// only directories are allowed:
if !meta.is_dir() {
return None;
}
let mut pbuf = dire.path();
// parse PID from /proc/XXXX
let pid: u32 = match pbuf.file_name() {
Some(fname) => {
let name = fname.to_string_lossy();
match name.parse() {
Ok(n) => n,
_ => return None,
}
},
_ => return None,
};
pbuf.push("exe");
let name: String = match pbuf.read_link() {
Ok(rpath) => {
match rpath.file_name() {
Some(path) => path.to_string_lossy().into_owned(),
_ => return None,
}
},
_ => return None,
};
if mcfg.appset.contains(&name) { Some(Appid::new(pid, name)) } else { None }
}).collect();
if apps.is_empty() { None } else { Some(apps) }
}
fn sysuptime() -> u64 {
let mut nowtim = timespec { tv_sec: 0, tv_nsec: 0 };
let _ = unsafe { clock_gettime(CLOCK_BOOTTIME, &mut nowtim as *mut timespec) };
let nowt: u64 = nowtim.tv_sec as u64;
nowt * 1000 + ((nowtim.tv_nsec / 1000000) as u64)
}
fn msleeptil(then: u64) {
let mut now = sysuptime();
while now < then {
std::thread::sleep(std::time::Duration::from_millis(then - now));
now = sysuptime();
}
}
fn timestr() -> String {
let mut nowtim = timespec { tv_sec: 0, tv_nsec: 0 };
let _ = unsafe { clock_gettime(CLOCK_REALTIME, &mut nowtim as *mut timespec) };
let nowdate = DateTime::<FixedOffset>::from_utc(
NaiveDateTime::from_timestamp_opt(nowtim.tv_sec as i64, 0).unwrap(),
FixedOffset::east_opt(8 * 60 * 60).unwrap());
format!("{}", nowdate.format("%Y%m%d-%H%M%S"))
}
fn main() {
let cfgfile: String = match std::env::args().skip(1).next() {
Some(arg1) => arg1,
None => "/tmp/mapsnap.json".to_string(),
};
let mcfgs = match Mapscfg::load_cfg(&cfgfile) {
Some(mcfg) => mcfg,
_ => {
eprintln!("Error, failed to load from '{}'.", cfgfile);
std::process::exit(1);
},
};
let mut msec = sysuptime();
loop {
let apps = match fetch_applist(&mcfgs) {
Some(list) => list,
_ => Vec::new(),
};
if apps.is_empty() {
eprintln!("{} Warning, No application of interest running!", timestr());
} else {
for app in apps {
let _ = app.snapshot(mcfgs.maproot.as_str());
}
unsafe { libc::sync() }; // flush filesystem cache after dumping maps
println!("-----------------------------------------------------------");
}
msec += (mcfgs.interval * 1000) as u64;
msleeptil(msec);
}
}
代码gdbdump.rs
的内容如下:
struct Dumprange {
start: usize,
end: usize,
}
struct Dumpargs {
pid: u32,
ranges: Vec<Dumprange>,
}
impl Dumprange {
fn new(range: &str) -> Option<Self> {
let errfunc = || {
eprintln!("Error, invalid gdb-dump range: {}", range);
};
let send: Vec<&str> = range.split('-').collect();
if send.len() != 0x2 {
errfunc();
return None;
}
let saddr = match usize::from_str_radix(send[0], 0x10) {
Ok(sa) => sa,
_ => { errfunc(); return None; },
};
let eaddr = match usize::from_str_radix(send[1], 0x10) {
Ok(ea) => ea,
_ => { errfunc(); return None; },
};
if saddr >= eaddr || saddr == 0 { None } else {
Some(Self { start: saddr, end: eaddr }) }
}
}
impl Dumpargs {
fn new(args: &[String]) -> Option<Self> {
let arg1 = args[0].as_str();
let pid: u32 = match str::parse::<u32>(arg1) {
Ok(val) if val > 0 => val,
_ => {
eprintln!("Error, invalid PID specified: {}", arg1);
return None;
},
};
let ranges: Vec<Dumprange> = args.iter().skip(1).filter_map(|arg| {
let argn = arg.as_str(); Dumprange::new(argn) }).collect();
if ranges.is_empty() { None } else { Some(Self { pid, ranges }) }
}
fn call_gdb(&self) -> i32 {
// older gdb does not support `--readnever:
let cmds: &[&str] = &["--nx", "--batch", "--readnever",
"-ex", "set pagination off", "-ex", "set height 0",
"-ex", "set width 0", "-ex"];
// create a gdb command:
let mut gdbc = std::process::Command::new("gdb");
let mut gdbr = gdbc.args(cmds)
.arg(format!("attach {}", self.pid));
for ran in &self.ranges {
let bin = format!("pid{}-{:#x}-{:#x}.bin", self.pid, ran.start, ran.end);
println!("Dumping memory to file: {} ...", bin);
let dump = format!("dump binary memory {} {:#x} {:#x}", bin, ran.start, ran.end);
gdbr = gdbr.arg("-ex").arg(&dump);
}
gdbr = gdbr.args(&["-ex", "detach", "-ex", "quit"]);
gdbr = gdbr.stdin(std::process::Stdio::null());
// spawn gdb child process
let mut res = match gdbr.spawn() {
Ok(child) => child,
Err(err) => {
eprintln!("Error, failed to invoke `gdb: {}", err);
return 1;
},
};
// wait until gdb child process exits
let res = match res.wait() {
Ok(est) => est,
Err(err) => {
eprintln!("Error, failed to wait gdb process: {}", err);
return 2;
},
};
// get the exit value of gdb child process
if let Some(eval) = res.code() { eval } else { 3 }
}
}
fn main() {
let argv: Vec<String> = std::env::args().skip(1).collect();
let argc = argv.len();
if argc < 2 {
eprintln!("Error, invalid number of command-line arguments: {}", argc);
std::process::exit(1);
}
let args = match Dumpargs::new(&argv) {
Some(darg) => darg,
None => std::process::exit(2),
};
println!("gdb-dump PID: {}", args.pid);
std::process::exit(args.call_gdb());
}