《操作系统》by李治军 | 实验3 - 系统调用

news2024/11/11 10:52:56

目录

一、实验内容

二、实验准备

1、系统调用的具体流程

(一)调用接口函数 API

(二)触发 0x80 号中断

(三)跳转到 system_call 函数

(四)执行系统调用函数 sys_xxx

2、总结概括实现系统调用的过程

三、正式实验

1. 添加系统调用 API

2. 添加系统调用号 + 修改系统调用总数

3. 维护系统调用表 + 编写系统调用函数(内核函数)

4. 修改 Makefile

5. make all

6. 编写测试程序

7. 拷贝 iam.c 和 whoami.c 到 Linux 0.11 目录

8. 启动虚拟机进行测试


一、实验内容

1、建立对系统调用接口的深入认识、掌握系统调用的基本过程、能完成系统调用的全面控制。

2、在 Linux 0.11 功能的基础上新添两个系统调用:iam() whoiam()

(1)第一个系统调用是 iam() ,其 API 原型为:

int iam(const char * name);
  • iam 函数实现将字符串参数 name 的内容拷贝到内核中保存下来
  • 要求 name 的长度不超过 23 个字符,返回值是拷贝的字符数。如果 name 的字符个数超过了 23 ,则返回 -1 ,并置 errno 为 EINVAL
  • kernal/who.c 中完成此系统调用的实现函数

(2)第二个系统调用是 whoami() ,其 API 原型为:

int whoami(char* name, unsigned int size);
  • whoami 函数实现将内核中由 iam() 保存的名字拷贝到 name 指向的用户地址空间中,同时确保不会对 name 越界访存(name 的大小由 size 说明)
  • 返回值是拷贝的字符数。如果 size 小于需要的空间,则返回 -1 ,并置 errno 为 EINVAL
  • kernal/who.c 中完成此系统调用的实现函数

3、测试程序。

        编写两个测试程序:iam.c whoami.c 。先运行添加过新编写的系统调用的 Linux 0.11,然后在 Linux 0.11 操作系统环境下编译运行这两个测试程序,测试新增的系统调用是否成功。

二、实验准备

  • 实验环境:Ubuntu 16.04 LTS

1、系统调用的具体流程

        通常情况下,应用程序想要调用系统调用和调用一个普通的自定义函数在代码上并没什么区别,但调用后发生的事情有很大不同。

(1)调用自定义函数,通过 call 指令直接跳转到该函数的地址,继续运行,结束后返回。

(2)调用系统调用,首先通过调用系统库中为该系统调用编写的一个接口函数,即 API(Application Programming Interface)- 应用程序编程接口,触发 0x80 号中断(即调用system_call 函数);然后根据系统调用编号跳转到对应的系统调用函数 system_xxx ,继续执行,结束后返回

(一)调用接口函数 API

        API 是一些预先定义好的函数,为的是提供给应用程序和开发人员基于某软件或硬件的以访问一组例程的能力,同时又无需访问源码,也无需理解内部工作机制的细节。API 并不能完成系统调用的真正功能,它要做的是通过 0x80 号中断去调用真正的系统调用,API 实现的内容:

  • 把系统调用的编号存入 EAX
  • 把接口函数的参数存入其它通用寄存器(EBX、ECX……)
  • 触发 0x80 号中断(int 0x80)

  Tip   linux-0.11/lib 目录下有一些已经实现的 API 。Linus 编写它们的原因是在内核加载完毕后,会切换到用户模式下,做一些初始化工作,然后启动 shell。而用户模式下的很多工作需要依赖一些系统调用才能完成,因此在内核中实现了这些系统调用的 API。

    lib/close.c 为例,分析一下 close() 的API:

/* linux-0.11/lib/close.c */

#define __LIBRARY__
#include <unistd.h>

_syscall1(int,close,int,fd)

(1)这里的 _syscall1 是一个带参宏,在 include/unistd.h 中定义:

#define _syscall1(type,name,atype,a) \
type name(atype a) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
	: "=a" (__res) \
	: "0" (__NR_##name),"b" ((long)(a))); \
if (__res >= 0) \
	return (type) __res; \
errno = -__res; \
return -1; \
}
  •  include/unistd.h 中有 linus 预先写好的系统调用 API 宏模板 _syscalln() ,其中 n 表示系统调用的参数个数
  • 如果对带参宏定义不太熟悉可以先看一下这篇帖子:宏定义(无参宏定义和带参宏定义),C语言宏定义详解

(2)进行宏展开后,得到 close() 较为完整的 API 代码(C语言内嵌汇编):

int close(int fd)
{
    long __res;
    __asm__ volatile ("int $0x80"
        : "=a" (__res)
        : "0" (__NR_close),"b" ((long)(fd)));
    if (__res >= 0)
        return (int) __res;
    errno = -__res;
    return -1;
}

① API 整体代码分析:

② 内嵌汇编代码分析:

(3)其中 __NR_close 是系统调用的编号。所有的系统调用都是通过 0x80 号中断进入系统内核,但是之后具体执行哪个系统调用函数就由调用编号决定(存在EAX中)。

  • 所以添加系统调用时还需要添加新增系统调用的编号,才能找到对应的系统调用函数
  • 系统调用编号在 include/unistd.h 中定义

(二)触发 0x80 号中断

        API 触发 0x80 号中断后,就要进行内核的中断处理,也就是调用 system_call 函数但 0x80 中断为什么就能跳转去执行 system_call 函数呢?——这其实就是在内核初始化时完成的工作。

所以我们可以先了解一下 Linux 0.11 处理 0x80 号中断的过程。

(1)内核初始化时,主函数 init/main.c 调用了 sched_init 初始化函数:

(2)sched_init kernel/sched.c 中定义,重点看最后一条语句:

(3)set_system_gate 是个宏,在 include/asm/system.h 中定义:

#define set_system_gate(n,addr) \
	_set_gate(&idt[n],15,3,addr)

(4)_set_gate 又是一个宏,也在 include/asm/system.h 中定义(C语言内嵌汇编):

#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
	"movw %0,%%dx\n\t" \
	"movl %%eax,%1\n\t" \
	"movl %%edx,%2" \
	: \
	: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
	"o" (*((char *) (gate_addr))), \
	"o" (*(4+(char *) (gate_addr))), \
	"d" ((char *) (addr)),"a" (0x00080000))

(5)上述使用了一系列的宏定义套娃,完全展开得到:

__asm__ ("movw %%dx,%%ax\n\t" \
	"movw %0,%%dx\n\t" \
	"movl %%eax,%1\n\t" \
	"movl %%edx,%2" \
	: \
	: "i" ((short) (0x8000+(3<<13)+(15<<8))), \
	"o" (*((char *) (idt[0x80]))), \
	"o" (*(4+(char *) (idt[0x80]))), \
	"d" ((char *) (&system_call)),"a" (0x00080000))

        看起来很复杂,但其实这段代码的功能就是填写 IDT - 中断描述符表 system_call 函数的地址写到了 0x80 中断对应的中断描述符中。所以之后我们调用 0x80 号中断,就会自动跳转到函数 system_call 的地址

(三)跳转到 system_call 函数

(1)system_call 函数定义在 kernel/system_call.s 中(纯汇编):

!……
! # 这是系统调用总数。如果增删了系统调用,必须做相应修改
nr_system_calls = 72
!……

! # system_call 用 .globl 修饰为其他函数可见
.globl system_call
.align 2
system_call:

! # 检查系统调用编号是否在合法范围内
    cmpl \$nr_system_calls-1,%eax
    ja bad_sys_call
    push %ds
    push %es
    push %fs
    pushl %edx
    pushl %ecx

! # push %ebx,%ecx,%edx,是传递给系统调用的参数
    pushl %ebx

! # 让ds, es指向GDT,内核地址空间
    movl $0x10,%edx
    mov %dx,%ds
    mov %dx,%es
    movl $0x17,%edx
! # 让fs指向LDT,用户地址空间
    mov %dx,%fs
    call sys_call_table(,%eax,4)
    pushl %eax
    movl current,%eax
    cmpl $0,state(%eax)
    jne reschedule
    cmpl $0,counter(%eax)
    je reschedule

我们重点研究倒数第 7 行的   call sys_call_table(,%eax,4)   语句,前面都是一些压栈保护操作。

根据汇编的寻址方法翻译得到: call sys_call_table + 4 * %eax 

  • 其中 eax 存放的是系统调用号 __NR_xxx ,也就是说 sys_call_table + 4 * __NR_xxx 就是相应的系统调用函数的入口

(2)sys_call_table include/linux/sys.h 中定义:

(3)fn_ptrinclude/linux/sched.h 中定义:

typedef int (*fn_ptr)();

        现在可以看出 sys_call_table 数组就是一个函数指针数组。数组名 sys_call_table 就是这个数组的起始地址sys_call_table + 4 * __NR_xxx 就是系统调用函数的入口,所以系统调用的函数在 sys_call_table 数组中的位置必须和  __NR_xxx 的值对应,才能跳转到正确的函数入口。

        另外 sys_call_table 数组中的所有系统调用的函数名统一规范为 sys_xxx ,这是我们学习和模仿的好对象。(所以我们添加的系统调用函数应该命名为 sys_iamsys.whoami

  • 如果对函数指针不熟悉可以先看看这篇文章:进阶C语言 - 指针(3):函数指针数组_渡上舟的博客-CSDN博客

(四)执行系统调用函数 sys_xxx

        system_call 中的  call sys_call_table(,%eax,4)  指令就是跳转去执行对应的系统调用函数 sys_xxx ,这个系统调用函数就是要实现系统调用的功能。

  例   fs/open.c 中的 sys_close 函数,其代码就是实现 close() 应有的功能:

2、总结概括实现系统调用的过程

  • 应用程序调用库函数(API)
  • API 将系统调用号存入 EAX,然后通过中断调用使系统进入内核态
  • 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用)
  • 系统调用完成相应功能,将返回值存入 EAX,返回到中断处理函数
  • 中断处理函数返回到 API 中
  • API 将 EAX 返回给应用程序

三、正式实验

1. 添加系统调用 API

        应用程序实现系统调用的第一步是调用库函数 API ,所以首先要添加 iam() whoami() 的 API 。这里可以直接使用预先写好的系统调用 API 宏模板 _syscalln() ,非常方便,所以我们暂时不添加,后面编写测试程序 iam.c 和 whoami.c 时再添加。

  • 添加 API 时注意事项:
/* 在应用程序中,要有: */

/* 有它,_syscall1 等才有效。详见unistd.h */
#define __LIBRARY__

/* 有它,编译器才能获知自定义的系统调用的编号 */
#include "unistd.h"

/* iam()在用户空间的接口函数 */
_syscall1(int, iam, const char*, name);

/* whoami()在用户空间的接口函数 */
_syscall2(int, whoami,char*,name,unsigned int,size);

2. 添加系统调用号 + 修改系统调用总数

        之后 API 将系统调用号存入 EAX,然后调用中断进入内核态。这里新增了两个系统调用,所以要添加新的系统调用号,还要修改系统调用总数。

(1)进入 linux-0.11/include 目录,打开 unistd.h ,增添新的系统调用编号。

  • 系统调用编号统一格式:__NR_xxx

(2)进入 linux-0.11/kernel 目录,打开 system_call.s ,修改系统调用总数。

3. 维护系统调用表 + 编写系统调用函数(内核函数)

        中断处理函数根据系统调用号,调用对应的内核函数,所以要为新增的系统调用添加系统调用函数名并维护系统调用表。

(1)进入 linux-0.11/include/linux 目录,打开 sys.h ,维护系统调用表:

  注   系统调用函数名在 sys_call_table 数组中的位置必须和 unistd.h 中 __NR_name 的值相同

(2)进入 linux-0.11/kernel 目录,创建一个 who.c 文件,为新增的系统调用函数编写代码,即实现 iam() whoami() 要求的功能。

#include <asm/segment.h>
#include <errno.h>
#include <string.h>

char _myname[24];

int sys_iam(const char *name)
{
    char str[25];
    int i = 0;

    do
    {
        // get char from user input
        str[i] = get_fs_byte(name + i);
    } while (i <= 25 && str[i++] != '\0');

    if (i > 24)
    {
        errno = EINVAL;
        i = -1;
    }
    else
    {
        // copy from user mode to kernel mode
        strcpy(_myname, str);
    }

    return i;
}

int sys_whoami(char *name, unsigned int size)
{
    int length = strlen(_myname);
    printk("%s\n", _myname);

    if (size < length)
    {
        errno = EINVAL;
        length = -1;
    }
    else
    {
        int i = 0;
        for (i = 0; i < length; i++)
        {
            // copy from kernel mode to user mode
            put_fs_byte(_myname[i], name + i);
        }
    }
    return length;
}

4. 修改 Makefile

        要想让我们添加的 kernel/who.c 和其它 Linux 代码编译链接到一起,必须要修改 Makefile 文件。

        Makefile 里记录的是所有源程序文件的编译、链接规则,《注释》3.6 节有简略介绍。我们之所以简单地运行  make  命令就可以实现编译整个代码树,是因为  make  完全按照 Makefile 里的指示进行工作。

        Makefile 在代码树中有很多,分别负责不同模块的编译工作。我们要修改的是 kernel/Makefile ,需要修改两处。

(1)第一处,在【OBJS】后添加 who.o 

(2)第二处,在【Dependencies】后添加 who.s who.o: who.c ../include/linux/kernel.h ../include/unistd.h 

  • 双击 Makefile 默认以 vim 打开,如果对 vim 编辑器不熟悉可以使用 gedit 进行编辑

5. make all

        Makefile 修改后,和往常一样在 linux-0.11 目录下执行  make all  命令就能自动把 who.c 加入到内核中了。

6. 编写测试程序

        到此为止,系统调用的内核函数已经完成。应用程序想要使用我们新增的系统调用 iamwhoami ,还需要添加对应的系统调用 API 。

  • 在 include/unistd.h 中,有预先写好的系统调用 API 宏模板 _syscalln() ,其中 n 表示系统调用的参数个数。根据 iam 和 whoami 的参数个数,可以看出应该分别使用 _syscall1 和 _syscall2

(1) oslab 目录下创建一个 iam.c whoami.c 文件

  • iam.c 完整代码:
/* iam.c */
#define __LIBRARY__
#include <unistd.h> 
#include <errno.h>
#include <asm/segment.h> 
#include <linux/kernel.h>

_syscall1(int, iam, const char*, name);
   
int main(int argc, char *argv[])
{
    /*调用系统调用iam()*/
    iam(argv[1]);
    return 0;
}
  • whoami.c 完整代码: 
/* whoami.c */
#define __LIBRARY__
#include <unistd.h> 
#include <errno.h>
#include <asm/segment.h> 
#include <linux/kernel.h>
#include <stdio.h>
   
_syscall2(int, whoami,char *,name,unsigned int,size);
   
int main(int argc, char *argv[])
{
    char username[64] = {0};
    /*调用系统调用whoami()*/
    whoami(username, 24);
    printf("%s\n", username);
    return 0;
}

7. 拷贝 iam.c 和 whoami.c 到 Linux 0.11 目录

        现在还不能直接编译运行,因为我们编写的 iam.c 和 whoami.c 还位于宿主机的 oslab 目录下,Linux 0.11 虚拟机目录下没有这两个文件,所以无法直接编译和运行。

(1)挂载

        以上两个文件需要放到启动后的 linux-0.11 操作系统上运行。这里可以采用 挂载 的方式实现宿主机与虚拟机操作系统的文件共享。

oslab 目录下执行以下命令挂载 hdc 目录到虚拟机操作系统上:

sudo ./mount-hdc

如果对挂载不熟悉可以先看看这篇文章:Linux 学习笔记(三):挂载 是什么_linux挂载的概念_Amentos的博客-CSDN博客

(2)拷贝       

oslab 目录下执行以下命令将上述两个文件拷贝到虚拟机 Linux 0.11 操作系统 /usr/root/ 目录下:

cp iam.c whoami.c hdc/usr/root

 拷贝成功!目标目录下存在对应的两个文件就可以启动虚拟机进行测试了。

8. 启动虚拟机进行测试

(1)编译

[/usr/root]# gcc -o iam iam.c
[/usr/root]# gcc -o whoami whoami.c

编译后显示:

说明之前修改的 unistd.h 没有加载到 linux 0.11 中,需要手动添加或直接拷贝。

 

(2)进入 hdc/usr/include/unistd.h ,为新增的系统调用添加系统调用号(需要先挂载):

(3)再次编译

没有返回信息,说明编译成功:

(4)运行

[/usr/root]# ./iam lqn
[/usr/root]# ./whoami

运行结果:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/416804.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

19从零开始学Java之局部变量和成员变量是怎么回事?

作者&#xff1a;孙玉昌&#xff0c;昵称【一一哥】&#xff0c;另外【壹壹哥】也是我哦 千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者 前言 在前两篇文章中&#xff0c;壹哥给大家讲解了Java里的条件分支&#xff0c;包括if和switch两种情况。…

23.Spring练习(spring、springMVC)

目录 一、Spring练习环境搭建。 &#xff08;1&#xff09;设置服务器启动的展示页面。 &#xff08;2&#xff09;创建工程步骤。 &#xff08;3&#xff09;applicationContext.xml配置文件。 &#xff08;4&#xff09;spring-mvc.xml配置文件。 &#xff08;5&#x…

Java集合——Set接口学习总结

一、HashSet实现类 1.常用方法 增加&#xff1a;add(E e)删除&#xff1a;remove(Object o)、clear()修改&#xff1a;查看&#xff1a;iterator()判断&#xff1a;contains(Object o)、isEmpty()常用遍历方式&#xff1a;Set<String> set new HashSet<String>()…

redis中布隆过滤器使用详解

文章目录一、布隆过滤器介绍1、什么是布隆过滤器2、布隆过滤器实现原理3、误判率4、布隆过滤器使用场景5、哈希表与布隆过滤器比较二、redis中布隆过滤器实战1.引入redisson依赖2.创建订单表3.配置redis4.配置BloomFilter5.创建订单6.单元测试总结一、布隆过滤器介绍 1、什么是…

什么是汽车以太网?

总目录链接>> AutoSAR入门和实战系列总目录 总目录链接>> AutoSAR BSW高阶配置系列总目录 文章目录什么是汽车以太网&#xff1f;汽车以太网市场中使用的标准和剖析汽车以太网类型什么是汽车以太网&#xff1f; 本页介绍了汽车以太网的基本特性并提到了汽车以…

【数据库】关系数据库

1.选择关系&#xff08;对行操作&#xff09; 2.投影&#xff08;对列操作&#xff09; &#xff08;行记录重复的不再显示&#xff09; 3.连接&#xff08;从两个关系的笛卡尔积中选出属性间满足一定条件的元组&#xff09; a.等值连接 b.自然连接&#xff08;等值连接的特殊…

【云原生Docker】08-Docker存储

【云原生|Docker】08-Docker存储 文章目录【云原生|Docker】08-Docker存储简介Docker存储挂载方式挂载方式介绍挂载主机目录数据卷容器特性Docker存储示例挂载主机目录Type: bindType: volume总结数据卷容器利用数据卷容器迁移数据备份数据&#xff1a;恢复数据&#xff1a;Doc…

【小程序】小程序组件-1

一. form组件的使用 这个点自己写的时候卡了好久&#xff0c;比较有感悟。 首先明确一点&#xff0c;为什么需要form。 form可以封装一个模块&#xff0c;这个模块里可以有多个input操作&#xff0c;多个输出接口&#xff0c;多个按键&#xff0c;但是至多有两个关键按键&am…

“QT快速上手指南”之计算器(一)Qt Creator,窗口组件

文章目录前言一、什么是QT&#xff1f;二、准备工作&#xff1a;1. 安装Qt Creator&#xff1a;2. 安装Qt SDK&#xff1a;3. 下载安装器&#xff1a;三、窗口组件&#xff1a;四、QT 基本组件的简单介绍&#xff1a;1. QWidget2. QPushButton3. QLabel4. QLineEdit5. QSpinBox…

智能火焰与烟雾检测系统(Python+YOLOv5深度学习模型+清新界面)

摘要&#xff1a;智能火焰与烟雾检测系统用于智能日常火灾检测报警&#xff0c;利用摄像头画面实时识别火焰与烟雾&#xff0c;另外支持图片、视频火焰检测并进行结果可视化。本文详细介绍基于智能火焰与烟雾检测系统&#xff0c;在介绍算法原理的同时&#xff0c;给出Python的…

FPGA基于XDMA实现PCIE X4视频采集HDMI输出 提供工程源码和QT上位机程序和技术支持

目录1、前言2、我已有的PCIE方案3、PCIE理论4、总体设计思路和方案5、vivado工程详解6、驱动安装7、QT上位机软件8、上板调试验证9、福利&#xff1a;工程代码的获取1、前言 PCIE&#xff08;PCI Express&#xff09;采用了目前业内流行的点对点串行连接&#xff0c;比起 PCI …

PE文件解析

PE结构图 工具101editor 文件实列 0-30h为DOS header 40-F0h为DOS STUB 100-1F0h为PE_HEADER 200-2B0h为SECTION_HEADER **虚拟地址&#xff1a;**文件加载到内存中&#xff0c;每个进程都有自己的4GB&#xff0c;这个4GB当中的某个位置叫做虚拟地址 基地址&#xff1a;文件…

23种设计模式之工厂方法模式(黑马程序员)

工厂方法模式一、概述二、结构三、实现四、总结在最后一、概述 定义一个用户创建对象的工厂(接口)&#xff0c;让接口的子类决定去实例化哪个对象&#xff0c;依赖其抽象编程。即工厂方法使一个类的实例化延迟到其工厂的子类。 二、结构 工厂方法模式的主要角色 抽象工厂&a…

计算机组成原理实验二 存储系统预习报告

实验一----静态RAM 一、实验目的 掌握静态随机存储器 RAM 工作特性及数据的读写方法。基于信号时序图&#xff0c;了解读写静态随机存储器的原理。 二、实验预习 1、阅读实验指导书&#xff0c;然后回答问题。 实验所用的静态存储器由一片 6116&#xff08;2K*8bit &#x…

ROS开发之如何在同一个节点订阅、处理、发布消息?

文章目录0、引言1、创建中间特殊文件&#xff08;含订阅者和发布者&#xff09;2、在CMakeLists.txt添加编译规则3、在launch添加启动项4、编译运行5、三维显示0、引言 在ROS应用一般会用到发布者和订阅者&#xff0c;若只接收传感器数据&#xff0c;则只实现订阅者就行&#x…

客户服务 KPI是什么

当企业着手改进其客户服务计划时&#xff0c;必须以可衡量的方式进行。因为如果我们为了改进而改进&#xff0c;没有衡量&#xff0c;我们就永远无法真正知道我们做得有多好&#xff01;如果您的公司已准备好升级其客户服务计划&#xff0c;这里有 12种方法可以衡量和跟踪您的客…

大学生问AI

大学生问AI写在最前面2&#xff0c;描述你在学习工作中碰到的最高级的 AI 是什么&#xff1f;1&#xff0c;你人生中第一次接触到 “人工智能” 的概念和产品是什么&#xff1f; 让你觉得 “人类做得东西的确有智能”&#xff1f;3&#xff0c;你听说过最近的 GPT&#xff0c;n…

Qt5.12實戰之Linux靜態庫與動態庫多文件生成a與so文件並調用

1.編輯並輸入內容到test.cpp與test2.cpp test.cpp #include <stdio.h> int func() {return 888; } test2.cpp #include <stdio.h> int func2() {return 999; } 將test.cpp與test2.cpp編譯成目標文件&#xff1a; g -c test.cpp test2.cpp 一次性生成目標文件…

阅读提升内在美

最近&#xff0c;想在内在美上提升自己&#xff0c;想把玩游戏的时间腾给阅读。不想只是善良&#xff0c;更希望自己拥有智慧拥有力量&#xff0c;更自信更热爱生活。 本篇博文会日日更新&#xff0c;也当作鼓励我继续阅读坚持阅读的动力和监督。 Z-Library 图书馆 分享一个…

LeetCode 1041. 困于环中的机器人

原题链接&#xff1a;1041. 困于环中的机器人 在无限的平面上&#xff0c;机器人最初位于 (0, 0) 处&#xff0c;面朝北方。注意: 北方向 是y轴的正方向。南方向 是y轴的负方向。东方向 是x轴的正方向。西方向 是x轴的负方向。 机器人可以接受下列三条指令之一&#xff1a; …