共计 4281 个字符,预计需要花费 11 分钟才能阅读完成。
作者:Hcamael@知道创宇 404 实验室
时间:2019 年 10 月 21 日
原文链接:https://paper.seebug.org/1060/
最近在搞路由器的时候,不小心把 CFE 给刷挂了,然后发现能通过 jtag 进行救砖,所以就对 jtag 进行了一波研究。
最开始只是想救砖,并没有想深入研究的想法。
救砖尝试
变砖的路由器型号为:LinkSys wrt54g v8
CPU 型号为:BCM5354
Flash 型号为:K8D6316UBM
首先通过 jtagulator 得到了设备上 jtag 接口的顺序。
正好公司有一个 jlink,但是参试了一波失败,识别不了设备。
随后通过 Google 搜到发现了一个工具叫: tjtag-pi
可以通树莓派来控制 jtag,随后学习了一波树莓派的操作。
树莓派 Pins
我使用的是 rpi3,其接口编号图如下:
或者在树莓派 3 中可以使用 gpio readall 查看各个接口的状态:
rpi3 中的 Python 有一个 RPi.GPIO 模块,可以控制这些接口。
举个例子:
from RPi import GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(2, GPIO.OUT)
GPIO.setup(3, GPIO.IN)
首先是需要进行初始化 GPIO 的模式,BCM 模式对应的针脚排序是上面图中橙色的部门。
然后可以对各个针脚进行单独设置,比如上图中,把 2 号针脚设置为输出,3 号针脚设置为输入。
GPIO.output(2, 1)
GPIO.output(2, 0)
使用 output 函数进行二进制输出GPIO.input(3)
1
使用 input 函数获取针脚的输入。
我们可以用线把两个针脚连起来测试上面的代码。
将树莓派对应针脚和路由器的连起来以后,可以运行 tjtag-pi 程序。但是在运行的过程中却遇到了问题,经常会卡在写 flash 的时候。通过调整配置,有时是可以写成功的,但是 CFE 并没有被救回来,备份 flash 的数据,发现并没有成功写入数据。
因为使用轮子失败,所以我只能自己尝试研究和造轮子了。
jtag
首先是针脚,我见过的设备给 jtag 一般是提供了 5 * 2 以上的引脚。其中有一般都是接地引脚,另一半只要知道 4 个最重要的引脚。
这四个引脚一般情况下的排序是:
TDI
TDO
TMS
TCK
TDI 表示输入,TDO 表示输出,TMS 控制位,TCK 时钟输入。
jtag 大致架构如上图所示,其中 TAP-Controller 的架构如下图所示:
根据上面这两个架构,对 jtag 的原理进行讲解。
jtag 的核心是 TAP-Controller,通过解析 TMS 数据,来决定输入和输出的关系。所以我们先来看看 TAP-Controller 的架构。
从上面的图中我们可以发现,在任何状态下,输出 5 次 1,都会回到 TEST LOGIC RESET 状态下。所以在使用 jtag 前,我们先通过 TMS 端口,发送 5 次为 1 的数据,jtag 的状态机将会进入到 RESET 的复原状态。
当 TAP 进入到 SHIFT-IR 的状态时,Instruction Register 将会开始接收 TDI 传入的数据,当输入结束后,进入到 UPDATE-IR 状态时将会解析指令寄存器的值,随后决定输出什么数据。
SHIFT-DR 则是控制数据寄存器,一般是在读写数据的时候需要使用。
讲到这里,就出现一个问题了,TMS 就一个端口,jtag 如何知道 TMS 每次输入的值是多少呢?这个时候就需要用到 TCK 端口了,该端口可以称为时钟指令。当 TCK 从低频变到高频时,获取一比特 TMS/TDI 输入,TDO 输出 1 比特。
比如我们让 TAP 进行一次复位操作:
for x in range(5):
TCK 0
TMS 1
TCK 1
再比如,我们需要给指令寄存器传入 0b10:
1. 复位
2. 进入 RUN-TEST/IDLE 状态
TCK 0
TMS 0
TCK 1
3. 进入 SELECT-DR-SCAN 状态
TCK 0
TMS 1
TCK 1
4. 进入 SELECT-IR-SCAN 状态
TCK 0
TMS 1
TCK 1
5. 进入 CAPTURE-IR 状态
TCK 0
TMS 0
TCK 1
6. 进入 SHIFT-IR 状态
TCK 0
TMS 0
TCK 1
7. 输入 0b10
TCK 0
TMS 0
TDI 0
TCK 1
TCK 0
TMS 1
TDI 1
TCK 0
随后就是进入 EXIT-IR -> UPDATE-IR
根据上面的理论我们就可以通过写一个设置 IR 的函数:
def clock(tms, tdi):
tms = 1 if tms else 0
tdi = 1 if tdi else 0
GPIO.output(TCK, 0)
GPIO.output(TMS, tms)
GPIO.output(TDI, tdi)
GPIO.output(TCK, 1)
return GPIO.input(TDO)
def reset():
clock(1, 0)
clock(1, 0)
clock(1, 0)
clock(1, 0)
clock(1, 0)
clock(0, 0)
def set_instr(instr):
clock(1, 0)
clock(1, 0)
clock(0, 0)
clock(0, 0)
for i in range(INSTR_LENGTH):
clock(i==(INSTR_LENGTH - 1), (instr>>i)&1)
clock(1, 0)
clock(0, 0)
把上面的代码理解清楚后,基本就理解了 TAP 的逻辑。接下来就是指令的问题了,指令寄存器的长度是多少?指令寄存器的值为多少时是有意义的?
不同的 CPU 对于上面的答案都不一样,通过我在网上搜索的结果,每个 CPU 应该都有一个 bsd(boundary scan description)文件。本篇文章研究的 CPU 型号是 BCM5354,但是我并没有在网上找到该型号 CPU 的 bsd 文件。我只能找了一个相同厂商不同型号的 CPU 的 bsd 文件进行参考。
bcm53101m.bsd
在该文件中我们能看到 jtag 端口在 cpu 端口的位置:
"tck : B46 ," &
"tdi : A57 ," &
"tdo : B47 ," &
"tms : A58 ," &
"trst_b : A59 ," &
attribute TAP_SCAN_RESET of trst_b : signal is true;
attribute TAP_SCAN_IN of tdi : signal is true;
attribute TAP_SCAN_MODE of tms : signal is true;
attribute TAP_SCAN_OUT of tdo : signal is true;
attribute TAP_SCAN_CLOCK of tck : signal is (2.5000000000000000000e+07, BOTH);
能找到指令长度的定义:
attribute INSTRUCTION_LENGTH of top: entity is 32;
能找到指令寄存器的有效值:
attribute INSTRUCTION_OPCODE of top: entity is
"IDCODE (11111111111111111111111111111110)," &
"BYPASS (00000000000000000000000000000000, 11111111111111111111111111111111)," &
"EXTEST (11111111111111111111111111101000)," &
"SAMPLE (11111111111111111111111111111000)," &
"PRELOAD (11111111111111111111111111111000)," &
"HIGHZ (11111111111111111111111111001111)," &
"CLAMP (11111111111111111111111111101111)" ;
当指令寄存器的值为 IDCODE 的时候,IDCODE 寄存器的输出通道开启,我们来看看 IDCODE 寄存器:
attribute IDCODE_REGISTER of top: entity is
"0000" & -- version
"0000000011011111" & -- part number
"00101111111" & -- manufacturer's identity"1"; -- required by 1149.1
从这里我们能看出 IDCODE 寄存器的固定输出为: 0b00000000000011011111001011111111
那我们怎么获取 TDO 的输出呢?这个时候数据寄存器 DR 就发挥作用了。
TAP 状态机切换到 SHIFT-IR
输出 IDCODE 到 IR 中
切换到 SHIFT-DR
获取 INSTRUCTION_LENGTH 长度的 TDO 输出值
退出
用代码形式的表示如下:
def ReadWriteData(data):
out_data = 0
clock(1, 0)
clock(0, 0)
clock(0, 0)
for i in range(32):
out_bit = clock((i == 31), ((data >> i) & 1))
out_data = out_data | (out_bit << i)
clock(1,0)
clock(0,0)
return out_data
def ReadData():
return ReadWriteData(0)
def WriteData(data):
ReadWriteData(data)
def idcode():
set_instr(INSTR_IDCODE)
print(hex(self.ReadData()))
因为我也是个初学者,边界扫描描述文件中的内容并不是都能看得懂,比如在边界扫描文件中并不能看出 BYPASS 指令是做什么的。但是在其他文档中,得知 BYPASS 寄存器一般是用来做测试的,在该寄存器中,输入和输出是直连,可以通过比较输入和输出的值,来判断端口是否连接正确。
另外还有边界扫描寄存器一大堆数据,也没完全研究透,相关的资料少的可怜。而且也找不到对应 CPU 的文档。
当研究到这里的时候,我只了解了 jtag 的基本原理,只会使用两个基本的指令(IDCODE, BYPASS)。但是对我修砖没任何帮助。
没办法,我又回头来看 tjtag 的源码,在 tjtag 中定义了几个指令寄存器的 OPCODE:
INSTR_ADDRESS = 0x08
INSTR_DATA = 0x09
INSTR_CONTROL = 0x0A
照抄着 tjtag 中 flash AMD 的操作,可以成功对 flash 进行擦除,写入操作读取操作。但是却不知其原理。
这里分享下我的脚本:jtag.py
flash 文档:https://www.dataman.com/media…
接下来将会对该 flash 文档进行研究,并在之后的文章中分享我后续的研究成果。