VSCode 远程调试嵌入式终端

使用 gdbserver 进行远程调试

Posted by Jerry Chen on May 18, 2020

工具的准备

  • VSCode:最新 Windows 桌面版;

  • Remote-SSH:VSCode 的 SSH 远程插件;

  • gdb 源码:用于编译 arm-linux-gdb,这步省略;

  • gdbserver 源码:用于编译在 arm 上运行的服务,一般在 gdb 源码的 gdbserver 目录下;

环境的准备

  • Windows 下安装好 VSCode;
  • VSCode 已安装 SSH 远程插件,并已经远程到 Ubuntu;

开始

下载 gdb 源码

强烈建议下载 9.2 或以后的版本,gdb 版本与 gcc 版本无关。

官方 ftp 下载

  • 我选择下载的是 gdb-9.1.tar.gz

  • 下载完成后拷贝到 Ubuntu 进行解压

    1
    
    tar -vxf gdb-9.1.tar.gz
    

编译 arm-linux-gdb

用于 arm 调试的 gdb

检查是否存在

首先看看你的系统中是否存在用于 arm 的 gdb 工具:

我的 scm-arm-linux-gnueabihf 交叉编译链就存在 scm-arm-linux-gnueabihf-gdb,恭喜你那就不用手动编译了;

版本是7.11.1

主机是 x86_64,目标是 arm

多写一句:scm-arm-linux-gnueabihf-xxx 就是 arm-linux-gnueabihf-xxx 的软链接。

手动编译真舒服

为防止以后仿真出现 “Remote ‘g’ packet reply is too long” 的问题,建议先看下一节打补丁;

如果你正在使用下面的交叉编译器,可以尝试手动编译,没有就先安装一种:

下面命令安装的版本太高,建议从 linaro 官网下载 4.9.4 版本使用; 如果需要卸载可使用 sudo apt-get remove --auto-remove gcc-arm-linux-gnueabihf

1
2
3
4
5
6
# arm-linux-gnueabihf-xxx
sudo apt-get install gcc-arm-linux-gnueabihf
sudo apt-get install g++-arm-linux-gnueabihf
# arm-linux-gnueabi-xxx
sudo apt-get install gcc-arm-linux-gnueabi
sudo apt-get install g++-arm-linux-gnueabi

接下来开始编译和安装:

1
2
3
4
5
6
7
8
9
10
cd gdb-9.1
mkdir my_work
cd my_work

# target 改为你的目标平台(arm)交叉编译链
# prefix 指定安装路径,推荐子目录或者/usr/local/arm/gdb
../configure --target=arm-linux-gnueabihf --prefix=$PWD/_install

make
make install

安装完成后,会多出一个 _install 文件夹:

在 _install 文件夹中可以找到我们的目标程序:

拷贝它到 /usr/bin/ 目录,方便后续使用:

1
sudo cp arm-linux-gnueabihf-gdb /usr/bin/

试着运行一次,显示主机是 pc 平台,目标是 arm 平台:

打补丁

为防止以后仿真出现 “Remote ‘g’ packet reply is too long” 的问题需要打如下补丁,gdb9.2 以及后续版本不需要打该补丁:

700-fix-remote-g-packet-reply-too-long.patch 补丁内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
--- a/gdb/remote.c
+++ b/gdb/remote.c
@@ -6110,8 +6110,19 @@ process_g_packet (struct regcache *regca
   buf_len = strlen (rs->buf);
 
   /* Further sanity checks, with knowledge of the architecture.  */
-  if (buf_len > 2 * rsa->sizeof_g_packet)
-    error (_("Remote 'g' packet reply is too long: %s"), rs->buf);
+  // if (buf_len > 2 * rsa->sizeof_g_packet)
+  //  error (_("Remote 'g' packet reply is too long: %s"), rs->buf);
+  if (buf_len > 2 * rsa->sizeof_g_packet) {
+    rsa->sizeof_g_packet = buf_len;
+    for (i = 0; i < gdbarch_num_regs (gdbarch); i++) {
+      if (rsa->regs[i].pnum == -1)
+        continue;
+      if (rsa->regs[i].offset >= rsa->sizeof_g_packet)
+        rsa->regs[i].in_g_packet = 0;
+      else
+        rsa->regs[i].in_g_packet = 1;
+    }
+  }

将 700-fix-remote-g-packet-reply-too-long.patch 搁到 toolchain/gdb/patches/ 目录下,接着编译即可;也可以按照补丁直接修改 gdb/remote.c 文件。

编译 gdbserver

arm 上运行的服务端

检查是否存在

我的系统中存在 scm-gdbserver

检查后看到该文件需要运行在 32 位 ARM 上,拷贝到开发板中试试;

手动编译真舒服

同样的,如果你正在使用下面的交叉编译器,可以尝试手动编译,没有就请就先安装一种:

1
2
3
4
5
6
# arm-linux-gnueabihf-xxx
sudo apt-get install gcc-arm-linux-gnueabihf
sudo apt-get install g++-arm-linux-gnueabihf
# arm-linux-gnueabi-xxx
sudo apt-get install gcc-arm-linux-gnueabi
sudo apt-get install g++-arm-linux-gnueabi

接下来开始编译 gdbserver:

1
2
3
4
5
6
7
cd gdb-9.1
cd gdb/gdbserver

# 这里是 host 改为你的交叉编译工具链
./configure --target=arm-linux --host=arm-linux-gnueabihf

make

编译出错的解决办法

1
2
make clean
make CXX=arm-linux-gnueabihf-g++

执行后发现当前目录生成了 gdbserver 可执行文件,且适用于 32 位 ARM 平台;

使用

解决网络问题

需要满足的条件:

确保 PC 和终端网络连接正常,且终端能访问到虚拟机。

我的网络情况如下:

PC IP:169.254.38.77(虚拟机 IP:192.168.118.132)

终端 IP:169.254.38.76

这时 PC 和虚拟机能 ping 通终端,但终端和虚拟机不在同一网段 ping 不通,所以这里可以加一个端口转发(一个端口能访问即可);

  1. 打开 “编辑” –> “虚拟网络编辑器” –> “选择 NAT 类型”的项

  2. 打开“NAT 设置”

  3. 添加一个新的端口转发

  4. 添加如下,这样终端访问 PC 的 7776 端口可以转发到虚拟机的 7776 端口,反之亦反

命令行方式调试

注意,gdbserver 需要和调试 host gdb 版本匹配

我这里运行:gdbserver_scm 和 scm-arm-linux-gnueabihf-gdb

  1. 在终端中运行 gdbserver

    1
    2
    3
    
    # 注意这里的 IP 是调试 host 端的 IP
    # mod_debug 是使用 -g 标志编译出来的可执行文件
    ./gdbserver_scm 169.254.38.77:7776 ./mod_debug
    

    执行后会处于监听本地 7776 端口,等待调试 host 端对应的 xxxgdb 进行连接;

  2. 在调试 host 端执行 xxxgdb 进入调试状态

    1
    
    scm-arm-linux-gnueabihf-gdb
    
  3. 连接远程 gdbserver 的 7776 端口

    不要复制粘贴,手动输入

    1
    
    (gdb) target remote 169.254.38.76:7776
    

    调试 host 端信息如下:

    终端 gdbserver 端打印信息如下:

  4. 按正常方式使用 xxxgdb 工具

    当 xxxgdb 退出后(输入 q 退出),gdbserver 也会退出;

在 VSCode 上调试

强烈建议先把终端的程序需要用到的库,拷贝到它的标准路径里。

新建调试配置

点击后会在 ${workspaceFolder}/.vscode/launch.json 中 “configurations” 键中新建一份新的配置,改配置名后会显示在选择框中,按 F5 就能按照所选配置进行执行;

新增的调试配置如下(“configurations” 键的值的一部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
    "name": "(arm-gdb) 启动",
    "type": "cppdbg",
    "request": "launch",
    "program": "${workspaceFolder}/LinuxPrjDemo/Prj/Linux/Output/Exe/main",
    "args": [],
    "stopAtEntry": false,
    "cwd": "${workspaceFolder}",
    "environment": [],
    "externalConsole": true,
    "MIMode": "gdb", 
    "miDebuggerPath": "/usr/bin/arm-linux-gnueabihf-gdb",
    "miDebuggerServerAddress": "169.254.38.76:7776",  
    "setupCommands": [
        {
            "description": "Enable pretty-printing for gdb",
            "text": "-enable-pretty-printing",
            "ignoreFailures": true
        }
    ]
},

更改要点:

  • “miDebuggerServerAddress” 改为终端 IP 和端口;
  • “miDebuggerPath” 改为你的 xxxgdb 路径;
  • “program” 改为你编译生成的可执行文件,需要和终端中被调试的文件相同;
开始调试
  1. 选择交叉 gdb 的配置;

  2. 在程序中打一个断点;

  3. 在终端中运行 gdbserver 服务(注意,调试一次后该服务会退出,需要再次执行才能进行下次调试);

    1
    
    ./gdbserver 169.254.38.77:7776 ./mod_debug
    
  4. 在 VSCode 中按下 F5 开始调试;

    VSCode 显示如下:

    被控终端串口控制台显示如下:

    最终的运行效果:

特别注意

我试过 tcp 远程控制开发板启动 gdbserver 的骚操作,可以启动,但是会影响运行结果,比如串口打不开。所以建议不要折腾,每次调试在开发板上手动运行下脚本开启 gdbserver。

附录

我的完整 launch.json

“运行” –> “打开配置”

  • “(gdb) 启动” 用于本地调试 x86 的编译结果;
  • “(arm-gdb) 启动” 用于远程调试 arm 的编译结果,需要远程 arm 开发板已启动 gdbserver;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{
    // 使用 IntelliSense 了解相关属性。 
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "(gdb) 启动",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/LinuxPrjDemo/Prj/Linux/Output/Exe/main",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "为 gdb 启用整齐打印",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ]
        },
        {
            "name": "(arm-gdb) 启动",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/LinuxPrjDemo/Prj/Linux/Output/Exe/main",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": true,
            "MIMode": "gdb", 
            "miDebuggerPath": "/usr/bin/arm-linux-gnueabihf-gdb",
            "miDebuggerServerAddress": "192.168.2.2:7777",  
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ]
        },
    ]
}

我的完整 tasks.json

“终端” –> “配置任务”

  • “make” 用于编译,执行了 automake.sh 脚本
  • “trans” 用于传输,调用了 file_send_client 传文件(后面有源码附录)
  • “trans activity file” 用于传输,传输当前活动的已打开文件
  • “local run activity file” 用于执行当前活动的已打开的 sh 后缀脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "label": "make",
            "type": "shell",
            "command": "cd ${workspaceFolder}/LinuxPrjDemo/Prj/Linux;./automake.sh",
            "problemMatcher": [],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        },
        {
            "label": "trans",
            "type": "shell",
            "command": "${workspaceFolder}/LinuxPrjDemo/Prj/Linux/Tools/x86/file_send_client",
            "args": [
                "192.168.2.2",
                "10069",
                "${workspaceFolder}/LinuxPrjDemo/Prj/Linux/Output/Exe/main",
                "/root/mywork",
                "mod",
                "0755"
            ],
        },
        {
            "label": "trans activity file",
            "type": "shell",
            "command": "${workspaceFolder}/LinuxPrjDemo/Prj/Linux/Tools/x86/file_send_client",
            "args": [
                "192.168.2.2",
                "10069",
                "${file}",
                "/root/mywork",
                "${fileBasename}",
                "0755"
            ],
        },
        {
            "label": "local run activity file",
            "type": "shell",
            "command": "chmod +x ${fileDirname}/${fileBasenameNoExtension}.sh;${fileDirname}/${fileBasenameNoExtension}.sh",
        },
    ],
}

我的快捷键绑定

“Ctrl + Shift + P” 输入 “open key”,选择打开键盘快捷方式(JSON)

  • “ctrl+shift+7” 绑定了 “make” 任务
  • “ctrl+shift+8” 绑定了 “trans” 任务
  • “ctrl+shift+9” 绑定了 “trans activity file” 任务
  • “ctrl+shift+0” 绑定了 “local run activity file” 任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 将键绑定放在此文件中以覆盖默认值
[
    {
        "key": "ctrl+shift+7",
        "command": "workbench.action.tasks.runTask",
        "args": "make"
    },
    {
        "key": "ctrl+shift+8",
        "command": "workbench.action.tasks.runTask",
        "args": "trans"
    },
    {
        "key": "ctrl+shift+9",
        "command": "workbench.action.tasks.runTask",
        "args": "trans activity file"
    },
    {
        "key": "ctrl+shift+0",
        "command": "workbench.action.tasks.runTask",
        "args": "local run activity file"
    }
]

我的传文件源码

file_recv_server.c

编译后放在开发板上,用于接收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
//编译:gcc file_recv_server.c -o file_recv_server -lpthread
//执行:./file_recv_server 0.0.0.0 10069
//用于接收,放在开发板

//程序参数
//参数1:IP
//参数2:Port

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h> //编译加 -pthread
#include <signal.h>
#include <errno.h>

//程序实现
//第一阶段接收参数信息,每条需回复"success"或者"fail"
//arg[0]: 板上目标文件夹,如不存在就创建
//arg[1]: 板上目标命名
//arg[2]: 板上目标长度
//arg[3]: 板上目标权限
//arg[4]: 传输完成后板上需要执行的脚本
//arg[5]: 脚本参数1
//arg[6]: 脚本参数2
//arg[X]: 脚本参数X
//arg[X+1]: end1

//第二阶段接收数据信息
//接收数据段1
//接收数据段2
//接收数据段N

void ProcessRequest(int new_sock, struct sockaddr_in *peer){
    char arg[16][256] = {0,}; //参数表
    int argIndex = 0; //参数索引
    int fd = 0; //文件句柄
    int fileLen = 0; //文件实际长度

    int state = 1; //第一阶段
	char buf1[256] = {0,}; //第一阶段Buf
	char buf2[8192] = {0,}; //第二阶段Buf

	//printf("client %s:%d connect~\n", inet_ntoa(peer->sin_addr),\
			ntohs(peer->sin_port));
    
	while(state > 0)
	{
        switch (state)
        {
        case 1:
        {
            ssize_t s = read(new_sock, buf1, sizeof(buf1) - 1);
            if(s > 0){
                if(strcmp(buf1, "end1") == 0){
                    if(argIndex >= 4){
                        state = 2; //进入第二阶段
                        write(new_sock, "success", 8);
                    }
                    else{
                        write(new_sock, "fail", 5);
                        state = 0; //退出
                    }
                    break;
                }
                memcpy(arg[argIndex++], buf1, s);
                write(new_sock, "success", 8);
            }
            else{
                if(s == -1 && errno == EAGAIN){ //超时
                    state = 0; //退出
                }
            }
        }
            break;
        case 2:
        {
            if(0 == fd){
                //如果路径不存在就建立路径
                if(access(arg[0], 0) == -1){
                    char cm[512] = "";
                    sprintf(cm, "mkdir -p %s", arg[0]);
                    system(cm);
                }
                //打开文件,准备写入
                char file[256] = "";
                if(arg[0][strlen(arg[0]) - 1] != '/'){
                    arg[0][strlen(arg[0])] = '/';
                }
                sprintf(file, "%s%s", arg[0], arg[1]);
                remove(file);

                /*获取命令行参数*/
                int fileMode = atoi(arg[3]);
                int mode_u = fileMode / 100;
                int mode_g = (fileMode - (mode_u*100)) / 10;
                int mode_o = fileMode - (mode_u*100) - (mode_g*10);
                fileMode = (mode_u * 8 * 8) + (mode_g * 8) + mode_o; // 八进制转换

                if(-1 == (fd = open(file, O_RDWR | O_CREAT, fileMode))){
                    state = 0; //退出
                    break;
                }
            }

            char fileBuf[8192] = {0,};
            int fileLen = 0;
            int s = 0;
            while((s = read(new_sock, fileBuf, sizeof(fileBuf) - 1)) > 0){
                write(fd, fileBuf, s); //写文件
                fileLen += s;
            }
            close(fd); //关闭
            state = 0; //准备退出

            //这里还有执行一个脚本
            if((argIndex >= 5) && (access(arg[4], 0) != -1)){
                chmod(arg[4], 0755);
                char sh[256] = "";
                memcpy(sh, arg[4], strlen(arg[4]));
                int i = 1;
                for(; i < argIndex - 4; i++){
                    sprintf(sh, "%s %s", sh, arg[4 + i]);
                }
                printf("runSh:{%s}(%d)\n", sh, (int)strlen(sh));
                system(sh);
            }
        }
            break;
        default:
            break;
        }
	}
}

typedef struct Arg{
    int fd;
    struct sockaddr_in addr;
}Arg;

void *CreateWorker(void* ptr){
	Arg* arg = (Arg*)ptr;
	ProcessRequest(arg->fd, &arg->addr);
	close(arg->fd);
	free(arg);
	return NULL;
}

int main(int argc, char *argv[]){
    if(3 != argc){
        printf("argc must be 3!\n");
        printf("Usage:\n  ./${this} ip port\n\n");
        exit(1);
    }
    
    char *p = argv[1];
    int port = atoi(argv[2]);

	int sfd = socket(AF_INET, SOCK_STREAM, 0);		//创建socket
	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = inet_addr(p);	//sin_addr:4字节二进制
	addr.sin_port = htons(port);			//sin_port:2字节二进制
	//上述结构体还会填充8字节0保持和struct sockaddr大小相同
	
	//服务端(可选):设置端口重用(否则断开连接后等2~4分钟才可用)
	int on = 1;
	if(setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1){
		perror("setsockopt");
		exit(1);
	}
    
	//服务端:将地址和socket绑定
	if((bind(sfd, (struct sockaddr *)&addr, sizeof(addr))) == -1){
		perror("bind");
		exit(1);
	}
	//服务端:监听,这样其他进程才能连接
	if((listen(sfd, 5)) == -1){
		perror("listen");
		exit(1);
	}
	
	signal(SIGPIPE, SIG_IGN);//屏蔽tcp管道异常断开产生的信号(默认杀进程)
	
	while(1){
		struct sockaddr_in peer;
		socklen_t len = sizeof(peer);
		
		int new_sock = accept(sfd,(struct sockaddr *)&peer,&len);
		if(new_sock == -1){
			perror("accept");
			exit(1);
		}

        //设置接收超时时间
        struct timeval timeout = {10, 0}; //10s
        if(setsockopt(new_sock,SOL_SOCKET,SO_RCVTIMEO,(const char*)&timeout,sizeof(timeout)) == -1){
            perror("setsockopt");
            exit(1);
        }

		pthread_t tid;
		Arg *arg = (Arg *)malloc(sizeof(Arg));
		arg->fd = new_sock;
		arg->addr = peer;
		//以下建立一个线程,要实现全双工请建立两个线程
		pthread_create(&tid, NULL, CreateWorker, (void*)arg);
		pthread_detach(tid);
	}
	return 0;
}

file_send_client.c

编译后放在 Ubuntu 上,用于发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//编译:gcc file_send_client.c -o file_send_client
//执行:./file_send_client 0.0.0.0 10069 ~/vscode/Other/hello ~/vscode/Other/test halo 0644
//执行:./file_send_client 0.0.0.0 10069 ~/vscode/Other/hello ~/vscode/Other/hahaha hel 0644 ~/vscode/Other/test.sh 123 heihei
//用于发送,放在虚拟机

//程序参数
//参数1:服务器 IP
//参数2:服务器 Port
//参数3:本地文件路径
//参数4:远程板上目标文件夹
//参数5:远程板上目标命名
//参数6:远程板上目标权限
//参数7:远程板上接收后需要执行的脚本
//参数8:远程板上接收后需要执行的脚本参数1
//参数9:远程板上接收后需要执行的脚本参数2
//参数X:远程板上接收后需要执行的脚本参数X

//程序实现
//第一阶段发送参数信息,每条有响应"success"或者"fail"
//arg[0]: 板上目标文件夹
//arg[1]: 板上目标命名
//arg[2]: 板上目标长度,等于本地文件长度
//arg[3]: 板上目标权限
//arg[4]: 传输完成后板上需要执行的脚本
//arg[5]: 脚本参数1
//arg[6]: 脚本参数2
//arg[X]: 脚本参数X
//arg[X+1]: end1

//第二阶段发送数据信息
//发送数据段1
//发送数据段2
//发送数据段N

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char *argv[]){
    if(7 > argc){
        printf("The number of parameters is at least 7!\n");
        printf("Usage:\n  ./${this} ip port local_file remote_dir remote_name remote_mod ...\n\n");
        exit(1);
    }
    
    char *p = argv[1];
    int port = atoi(argv[2]);
    int fd = open(argv[3], O_RDONLY, 0644); //本地文件句柄
    if(fd == -1){
        perror("open");
        exit(1);
    }
    struct stat statbuff;
	stat(argv[3], &statbuff);
    int fileLen = statbuff.st_size; //本地文件长度

	int sfd = socket(AF_INET, SOCK_STREAM, 0);		//创建socket,端口随机
	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = inet_addr(p);	//sin_addr:4字节二进制
	addr.sin_port = htons(port);			//sin_port:2字节二进制
	//上述结构体还会填充8字节0保持和struct sockaddr大小相同
	
	//客户端:根据远程addr进行连接
	if((connect(sfd, (struct sockaddr *)&addr, sizeof(addr))) == -1){
		perror("connect");
        close(sfd);
        close(fd);
		exit(1);
	}
	
    int argIndex = 0; //参数索引
    int fileCurSend = 0; //当前发送文件长度
    int state = 1; //第一阶段
	char buf[16] = {0,}; //响应数组

	while(state > 0){
		switch (state)
        {
        case 1:
        {
            if(argIndex == 0){
                write(sfd, argv[4], strlen(argv[4]) + 1); //远程板上目标文件夹
            }
            else if(argIndex == 1){
                write(sfd, argv[5], strlen(argv[5]) + 1); //远程板上目标命名
            }
            else if(argIndex == 2){
                char len_str[32] = "";
                sprintf(len_str, "%d", fileLen);
                write(sfd, len_str, strlen(len_str) + 1); //远程板上目标长度
            }
            else if(argIndex == 3){
                write(sfd, argv[6], strlen(argv[6]) + 1); //远程板上目标权限
            }
            else if(argIndex + 3 < argc){
                write(sfd, argv[argIndex + 3], strlen(argv[argIndex + 3]) + 1); //其他
            }
            else{
                write(sfd, "end1", 5); //第一阶段结束
            }

            ssize_t s = read(sfd, buf, sizeof(buf) - 1);
            if(s > 0){
                if(strcmp(buf, "success") == 0){
                    if(argIndex + 3 >= argc){
                        state = 2; //进入第二阶段
                        break;
                    }
                    argIndex++;
                }
                else if(strcmp(buf, "fail") == 0){
                    close(sfd);
                    close(fd);
                    printf("step 1 failed!\n");
                    exit(1);
                }
            }
        }
            break;
        case 2:
        {
            char fileBuf[8192] = {0,};
            int fileLen = 0;
            int s = 0;
            while((s = read(fd, fileBuf, sizeof(fileBuf) - 1)) > 0){
                write(sfd, fileBuf, s); //发文件
                fileLen += s;
            }

            state = 0; //准备退出
        }
            break;
        
        default:
            break;
        }
	}
    close(sfd);
    close(fd);
	return 0;
}

定位段错误的方法

使用字符 gdb 可以快速定位段错误

  1. 被测终端 gdbserver :7777 problem_demo
  2. 虚拟机 arm-linux-gnueabihf-gdb
    • target remote 192.168.2.1:7777 连接被测终端的 gdbserver
    • c 继续执行
    • 等待发生段错误
    • bt 查看进程调用栈,会打印某个文件第几行出现问题
  3. 也可以直接在被测终端上 gdb problem_demo 进行调试,段错误后 bt
  4. 或者开启核心转储 coredump,段错误后会生成 core.<pid> 的文件,gdb -c core_file,进去后输入 where 就可查到出错行
  5. 调试正在运行的进程可使用 gdb attach <pid>