fuzz_arm: Add fuzzing for thumb instructions
This commit is contained in:
parent
8b612edb75
commit
331a02e02e
3 changed files with 172 additions and 36 deletions
|
@ -36,8 +36,8 @@
|
||||||
namespace {
|
namespace {
|
||||||
using namespace Dynarmic;
|
using namespace Dynarmic;
|
||||||
|
|
||||||
bool ShouldTestInst(u32 instruction, u32 pc, bool is_last_inst) {
|
bool ShouldTestInst(u32 instruction, u32 pc, bool is_thumb, bool is_last_inst) {
|
||||||
const A32::LocationDescriptor location{pc, {}, {}};
|
const A32::LocationDescriptor location = A32::LocationDescriptor{pc, {}, {}}.SetTFlag(is_thumb);
|
||||||
IR::Block block{location};
|
IR::Block block{location};
|
||||||
const bool should_continue = A32::TranslateSingleInstruction(block, location, instruction);
|
const bool should_continue = A32::TranslateSingleInstruction(block, location, instruction);
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ bool ShouldTestInst(u32 instruction, u32 pc, bool is_last_inst) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 GenRandomInst(u32 pc, bool is_last_inst) {
|
u32 GenRandomArmInst(u32 pc, bool is_last_inst) {
|
||||||
static const struct InstructionGeneratorInfo {
|
static const struct InstructionGeneratorInfo {
|
||||||
std::vector<InstructionGenerator> generators;
|
std::vector<InstructionGenerator> generators;
|
||||||
std::vector<InstructionGenerator> invalid;
|
std::vector<InstructionGenerator> invalid;
|
||||||
|
@ -139,13 +139,58 @@ u32 GenRandomInst(u32 pc, bool is_last_inst) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ShouldTestInst(inst, pc, is_last_inst)) {
|
if (ShouldTestInst(inst, pc, false, is_last_inst)) {
|
||||||
return inst;
|
return inst;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Dynarmic::A32::UserConfig GetUserConfig(ArmTestEnv& testenv) {
|
std::vector<u16> GenRandomThumbInst(u32 pc, bool is_last_inst) {
|
||||||
|
static const struct InstructionGeneratorInfo {
|
||||||
|
std::vector<InstructionGenerator> generators;
|
||||||
|
std::vector<InstructionGenerator> invalid;
|
||||||
|
} instructions = []{
|
||||||
|
const std::vector<std::tuple<std::string, const char*>> list {
|
||||||
|
#define INST(fn, name, bitstring) {#fn, bitstring},
|
||||||
|
#include "frontend/A32/decoder/thumb16.inc"
|
||||||
|
#include "frontend/A32/decoder/thumb32.inc"
|
||||||
|
#undef INST
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<InstructionGenerator> generators;
|
||||||
|
std::vector<InstructionGenerator> invalid;
|
||||||
|
|
||||||
|
// List of instructions not to test
|
||||||
|
static constexpr std::array do_not_test {
|
||||||
|
"thumb16_SETEND",
|
||||||
|
"thumb16_BKPT",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& [fn, bitstring] : list) {
|
||||||
|
if (std::find(do_not_test.begin(), do_not_test.end(), fn) != do_not_test.end()) {
|
||||||
|
invalid.emplace_back(InstructionGenerator{bitstring});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
generators.emplace_back(InstructionGenerator{bitstring});
|
||||||
|
}
|
||||||
|
return InstructionGeneratorInfo{generators, invalid};
|
||||||
|
}();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const size_t index = RandInt<size_t>(0, instructions.generators.size() - 1);
|
||||||
|
const u32 inst = instructions.generators[index].Generate();
|
||||||
|
const bool is_four_bytes = (inst >> 16) != 0;
|
||||||
|
|
||||||
|
if (ShouldTestInst(is_four_bytes ? Common::SwapHalves32(inst) : inst, pc, true, is_last_inst)) {
|
||||||
|
if (is_four_bytes)
|
||||||
|
return { static_cast<u16>(inst >> 16), static_cast<u16>(inst) };
|
||||||
|
return { static_cast<u16>(inst) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename TestEnv>
|
||||||
|
Dynarmic::A32::UserConfig GetUserConfig(TestEnv& testenv) {
|
||||||
Dynarmic::A32::UserConfig user_config;
|
Dynarmic::A32::UserConfig user_config;
|
||||||
user_config.optimizations &= ~OptimizationFlag::FastDispatch;
|
user_config.optimizations &= ~OptimizationFlag::FastDispatch;
|
||||||
user_config.callbacks = &testenv;
|
user_config.callbacks = &testenv;
|
||||||
|
@ -153,22 +198,28 @@ Dynarmic::A32::UserConfig GetUserConfig(ArmTestEnv& testenv) {
|
||||||
return user_config;
|
return user_config;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void RunTestInstance(Dynarmic::A32::Jit& jit, A32Unicorn<ArmTestEnv>& uni,
|
template <typename TestEnv>
|
||||||
ArmTestEnv& jit_env, ArmTestEnv& uni_env,
|
static void RunTestInstance(Dynarmic::A32::Jit& jit,
|
||||||
const A32Unicorn<ArmTestEnv>::RegisterArray& regs,
|
A32Unicorn<TestEnv>& uni,
|
||||||
const A32Unicorn<ArmTestEnv>::ExtRegArray& vecs,
|
TestEnv& jit_env,
|
||||||
const std::vector<u32>& instructions, const u32 cpsr, const u32 fpscr) {
|
TestEnv& uni_env,
|
||||||
|
const typename A32Unicorn<TestEnv>::RegisterArray& regs,
|
||||||
|
const typename A32Unicorn<TestEnv>::ExtRegArray& vecs,
|
||||||
|
const std::vector<typename TestEnv::InstructionType>& instructions,
|
||||||
|
const u32 cpsr,
|
||||||
|
const u32 fpscr,
|
||||||
|
const size_t ticks_left) {
|
||||||
const u32 initial_pc = regs[15];
|
const u32 initial_pc = regs[15];
|
||||||
const u32 num_words = initial_pc / sizeof(u32);
|
const u32 num_words = initial_pc / sizeof(typename TestEnv::InstructionType);
|
||||||
const u32 code_mem_size = num_words + static_cast<u32>(instructions.size());
|
const u32 code_mem_size = num_words + static_cast<u32>(instructions.size());
|
||||||
|
|
||||||
jit_env.code_mem.resize(code_mem_size + 1);
|
jit_env.code_mem.resize(code_mem_size);
|
||||||
uni_env.code_mem.resize(code_mem_size + 1);
|
uni_env.code_mem.resize(code_mem_size);
|
||||||
|
|
||||||
std::copy(instructions.begin(), instructions.end(), jit_env.code_mem.begin() + num_words);
|
std::copy(instructions.begin(), instructions.end(), jit_env.code_mem.begin() + num_words);
|
||||||
std::copy(instructions.begin(), instructions.end(), uni_env.code_mem.begin() + num_words);
|
std::copy(instructions.begin(), instructions.end(), uni_env.code_mem.begin() + num_words);
|
||||||
jit_env.code_mem.back() = 0xEAFFFFFE; // B .
|
jit_env.PadCodeMem();
|
||||||
uni_env.code_mem.back() = 0xEAFFFFFE; // B .
|
uni_env.PadCodeMem();
|
||||||
jit_env.modified_memory.clear();
|
jit_env.modified_memory.clear();
|
||||||
uni_env.modified_memory.clear();
|
uni_env.modified_memory.clear();
|
||||||
jit_env.interrupts.clear();
|
jit_env.interrupts.clear();
|
||||||
|
@ -186,10 +237,23 @@ static void RunTestInstance(Dynarmic::A32::Jit& jit, A32Unicorn<ArmTestEnv>& uni
|
||||||
uni.SetCpsr(cpsr);
|
uni.SetCpsr(cpsr);
|
||||||
uni.ClearPageCache();
|
uni.ClearPageCache();
|
||||||
|
|
||||||
jit_env.ticks_left = instructions.size();
|
jit_env.ticks_left = ticks_left;
|
||||||
jit.Run();
|
jit.Run();
|
||||||
|
|
||||||
uni_env.ticks_left = instructions.size();
|
uni_env.ticks_left = [&]{
|
||||||
|
if constexpr (std::is_same_v<TestEnv, ThumbTestEnv>) {
|
||||||
|
// Unicorn counts thumb instructions weirdly:
|
||||||
|
// A 32-bit thumb instruction counts as two.
|
||||||
|
// Except for branch instructions which count as one???
|
||||||
|
if (instructions.size() <= 1)
|
||||||
|
return ticks_left;
|
||||||
|
if ((instructions[instructions.size() - 2] & 0xF800) <= 0xE800)
|
||||||
|
return instructions.size();
|
||||||
|
return instructions.size() - 1;
|
||||||
|
} else {
|
||||||
|
return ticks_left;
|
||||||
|
}
|
||||||
|
}();
|
||||||
uni.Run();
|
uni.Run();
|
||||||
|
|
||||||
SCOPE_FAIL {
|
SCOPE_FAIL {
|
||||||
|
@ -279,7 +343,7 @@ static void RunTestInstance(Dynarmic::A32::Jit& jit, A32Unicorn<ArmTestEnv>& uni
|
||||||
}
|
}
|
||||||
} // Anonymous namespace
|
} // Anonymous namespace
|
||||||
|
|
||||||
TEST_CASE("A32: Single random instruction", "[arm]") {
|
TEST_CASE("A32: Single random arm instruction", "[arm]") {
|
||||||
ArmTestEnv jit_env{};
|
ArmTestEnv jit_env{};
|
||||||
ArmTestEnv uni_env{};
|
ArmTestEnv uni_env{};
|
||||||
|
|
||||||
|
@ -294,7 +358,7 @@ TEST_CASE("A32: Single random instruction", "[arm]") {
|
||||||
std::generate(regs.begin(), regs.end(), [] { return RandInt<u32>(0, ~u32(0)); });
|
std::generate(regs.begin(), regs.end(), [] { return RandInt<u32>(0, ~u32(0)); });
|
||||||
std::generate(ext_reg.begin(), ext_reg.end(), [] { return RandInt<u32>(0, ~u32(0)); });
|
std::generate(ext_reg.begin(), ext_reg.end(), [] { return RandInt<u32>(0, ~u32(0)); });
|
||||||
|
|
||||||
instructions[0] = GenRandomInst(0, true);
|
instructions[0] = GenRandomArmInst(0, true);
|
||||||
|
|
||||||
const u32 start_address = 100;
|
const u32 start_address = 100;
|
||||||
const u32 cpsr = (RandInt<u32>(0, 0xF) << 28) | 0x10;
|
const u32 cpsr = (RandInt<u32>(0, 0xF) << 28) | 0x10;
|
||||||
|
@ -303,11 +367,11 @@ TEST_CASE("A32: Single random instruction", "[arm]") {
|
||||||
INFO("Instruction: 0x" << std::hex << instructions[0]);
|
INFO("Instruction: 0x" << std::hex << instructions[0]);
|
||||||
|
|
||||||
regs[15] = start_address;
|
regs[15] = start_address;
|
||||||
RunTestInstance(jit, uni, jit_env, uni_env, regs, ext_reg, instructions, cpsr, fpcr);
|
RunTestInstance(jit, uni, jit_env, uni_env, regs, ext_reg, instructions, cpsr, fpcr, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("A32: Small random block", "[arm]") {
|
TEST_CASE("A32: Small random arm block", "[arm]") {
|
||||||
ArmTestEnv jit_env{};
|
ArmTestEnv jit_env{};
|
||||||
ArmTestEnv uni_env{};
|
ArmTestEnv uni_env{};
|
||||||
|
|
||||||
|
@ -322,11 +386,11 @@ TEST_CASE("A32: Small random block", "[arm]") {
|
||||||
std::generate(regs.begin(), regs.end(), [] { return RandInt<u32>(0, ~u32(0)); });
|
std::generate(regs.begin(), regs.end(), [] { return RandInt<u32>(0, ~u32(0)); });
|
||||||
std::generate(ext_reg.begin(), ext_reg.end(), [] { return RandInt<u32>(0, ~u32(0)); });
|
std::generate(ext_reg.begin(), ext_reg.end(), [] { return RandInt<u32>(0, ~u32(0)); });
|
||||||
|
|
||||||
instructions[0] = GenRandomInst(0, false);
|
instructions[0] = GenRandomArmInst(0, false);
|
||||||
instructions[1] = GenRandomInst(4, false);
|
instructions[1] = GenRandomArmInst(4, false);
|
||||||
instructions[2] = GenRandomInst(8, false);
|
instructions[2] = GenRandomArmInst(8, false);
|
||||||
instructions[3] = GenRandomInst(12, false);
|
instructions[3] = GenRandomArmInst(12, false);
|
||||||
instructions[4] = GenRandomInst(16, true);
|
instructions[4] = GenRandomArmInst(16, true);
|
||||||
|
|
||||||
const u32 start_address = 100;
|
const u32 start_address = 100;
|
||||||
const u32 cpsr = (RandInt<u32>(0, 0xF) << 28) | 0x10;
|
const u32 cpsr = (RandInt<u32>(0, 0xF) << 28) | 0x10;
|
||||||
|
@ -339,11 +403,11 @@ TEST_CASE("A32: Small random block", "[arm]") {
|
||||||
INFO("Instruction 5: 0x" << std::hex << instructions[4]);
|
INFO("Instruction 5: 0x" << std::hex << instructions[4]);
|
||||||
|
|
||||||
regs[15] = start_address;
|
regs[15] = start_address;
|
||||||
RunTestInstance(jit, uni, jit_env, uni_env, regs, ext_reg, instructions, cpsr, fpcr);
|
RunTestInstance(jit, uni, jit_env, uni_env, regs, ext_reg, instructions, cpsr, fpcr, 5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("A32: Large random block", "[arm]") {
|
TEST_CASE("A32: Large random arm block", "[arm]") {
|
||||||
ArmTestEnv jit_env{};
|
ArmTestEnv jit_env{};
|
||||||
ArmTestEnv uni_env{};
|
ArmTestEnv uni_env{};
|
||||||
|
|
||||||
|
@ -361,7 +425,7 @@ TEST_CASE("A32: Large random block", "[arm]") {
|
||||||
std::generate(ext_reg.begin(), ext_reg.end(), [] { return RandInt<u32>(0, ~u32(0)); });
|
std::generate(ext_reg.begin(), ext_reg.end(), [] { return RandInt<u32>(0, ~u32(0)); });
|
||||||
|
|
||||||
for (size_t j = 0; j < instruction_count; ++j) {
|
for (size_t j = 0; j < instruction_count; ++j) {
|
||||||
instructions[j] = GenRandomInst(j * 4, j == instruction_count - 1);
|
instructions[j] = GenRandomArmInst(j * 4, j == instruction_count - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const u64 start_address = 100;
|
const u64 start_address = 100;
|
||||||
|
@ -369,6 +433,64 @@ TEST_CASE("A32: Large random block", "[arm]") {
|
||||||
const u32 fpcr = RandomFpcr();
|
const u32 fpcr = RandomFpcr();
|
||||||
|
|
||||||
regs[15] = start_address;
|
regs[15] = start_address;
|
||||||
RunTestInstance(jit, uni, jit_env, uni_env, regs, ext_reg, instructions, cpsr, fpcr);
|
RunTestInstance(jit, uni, jit_env, uni_env, regs, ext_reg, instructions, cpsr, fpcr, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A32: Single random thumb instruction", "[thumb]") {
|
||||||
|
ThumbTestEnv jit_env{};
|
||||||
|
ThumbTestEnv uni_env{};
|
||||||
|
|
||||||
|
Dynarmic::A32::Jit jit{GetUserConfig(jit_env)};
|
||||||
|
A32Unicorn<ThumbTestEnv> uni{uni_env};
|
||||||
|
|
||||||
|
A32Unicorn<ThumbTestEnv>::RegisterArray regs;
|
||||||
|
A32Unicorn<ThumbTestEnv>::ExtRegArray ext_reg;
|
||||||
|
std::vector<u16> instructions;
|
||||||
|
|
||||||
|
for (size_t iteration = 0; iteration < 100000; ++iteration) {
|
||||||
|
std::generate(regs.begin(), regs.end(), [] { return RandInt<u32>(0, ~u32(0)); });
|
||||||
|
std::generate(ext_reg.begin(), ext_reg.end(), [] { return RandInt<u32>(0, ~u32(0)); });
|
||||||
|
|
||||||
|
instructions = GenRandomThumbInst(0, true);
|
||||||
|
|
||||||
|
const u32 start_address = 100;
|
||||||
|
const u32 cpsr = (RandInt<u32>(0, 0xF) << 28) | 0x1F0;
|
||||||
|
const u32 fpcr = RandomFpcr();
|
||||||
|
|
||||||
|
INFO("Instruction: 0x" << std::hex << instructions[0]);
|
||||||
|
|
||||||
|
regs[15] = start_address;
|
||||||
|
RunTestInstance(jit, uni, jit_env, uni_env, regs, ext_reg, instructions, cpsr, fpcr, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A32: Small random thumb block", "[thumb]") {
|
||||||
|
ThumbTestEnv jit_env{};
|
||||||
|
ThumbTestEnv uni_env{};
|
||||||
|
|
||||||
|
Dynarmic::A32::Jit jit{GetUserConfig(jit_env)};
|
||||||
|
A32Unicorn<ThumbTestEnv> uni{uni_env};
|
||||||
|
|
||||||
|
A32Unicorn<ThumbTestEnv>::RegisterArray regs;
|
||||||
|
A32Unicorn<ThumbTestEnv>::ExtRegArray ext_reg;
|
||||||
|
std::vector<u16> instructions;
|
||||||
|
|
||||||
|
for (size_t iteration = 0; iteration < 100000; ++iteration) {
|
||||||
|
std::generate(regs.begin(), regs.end(), [] { return RandInt<u32>(0, ~u32(0)); });
|
||||||
|
std::generate(ext_reg.begin(), ext_reg.end(), [] { return RandInt<u32>(0, ~u32(0)); });
|
||||||
|
|
||||||
|
instructions.clear();
|
||||||
|
for (size_t i = 0; i < 5; i++) {
|
||||||
|
const std::vector<u16> inst = GenRandomThumbInst(instructions.size() * 2, i == 4);
|
||||||
|
instructions.insert(instructions.end(), inst.begin(), inst.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
const u32 start_address = 100;
|
||||||
|
const u32 cpsr = (RandInt<u32>(0, 0xF) << 28) | 0x1F0;
|
||||||
|
const u32 fpcr = RandomFpcr();
|
||||||
|
|
||||||
|
regs[15] = start_address;
|
||||||
|
RunTestInstance(jit, uni, jit_env, uni_env, regs, ext_reg, instructions, cpsr, fpcr, 5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,19 +16,27 @@
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
|
||||||
template <typename InstructionType_, u32 infinite_loop>
|
template <typename InstructionType_, u32 infinite_loop_u32>
|
||||||
class A32TestEnv final : public Dynarmic::A32::UserCallbacks {
|
class A32TestEnv final : public Dynarmic::A32::UserCallbacks {
|
||||||
public:
|
public:
|
||||||
using InstructionType = InstructionType_;
|
using InstructionType = InstructionType_;
|
||||||
using RegisterArray = std::array<u32, 16>;
|
using RegisterArray = std::array<u32, 16>;
|
||||||
using ExtRegsArray = std::array<u32, 64>;
|
using ExtRegsArray = std::array<u32, 64>;
|
||||||
|
|
||||||
|
static constexpr InstructionType infinite_loop = static_cast<InstructionType>(infinite_loop_u32);
|
||||||
|
|
||||||
u64 ticks_left = 0;
|
u64 ticks_left = 0;
|
||||||
bool code_mem_modified_by_guest = false;
|
bool code_mem_modified_by_guest = false;
|
||||||
std::vector<InstructionType> code_mem;
|
std::vector<InstructionType> code_mem;
|
||||||
std::map<u32, u8> modified_memory;
|
std::map<u32, u8> modified_memory;
|
||||||
std::vector<std::string> interrupts;
|
std::vector<std::string> interrupts;
|
||||||
|
|
||||||
|
void PadCodeMem() {
|
||||||
|
do {
|
||||||
|
code_mem.push_back(infinite_loop);
|
||||||
|
} while (code_mem.size() % 2 != 0);
|
||||||
|
}
|
||||||
|
|
||||||
std::uint32_t MemoryReadCode(u32 vaddr) override {
|
std::uint32_t MemoryReadCode(u32 vaddr) override {
|
||||||
const size_t index = vaddr / sizeof(InstructionType);
|
const size_t index = vaddr / sizeof(InstructionType);
|
||||||
if (index < code_mem.size()) {
|
if (index < code_mem.size()) {
|
||||||
|
@ -36,7 +44,7 @@ public:
|
||||||
std::memcpy(&value, &code_mem[index], sizeof(u32));
|
std::memcpy(&value, &code_mem[index], sizeof(u32));
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
return infinite_loop; // B .
|
return infinite_loop_u32; // B .
|
||||||
}
|
}
|
||||||
|
|
||||||
std::uint8_t MemoryRead8(u32 vaddr) override {
|
std::uint8_t MemoryRead8(u32 vaddr) override {
|
||||||
|
@ -81,7 +89,7 @@ public:
|
||||||
|
|
||||||
void CallSVC(std::uint32_t swi) override { ASSERT_MSG(false, "CallSVC({})", swi); }
|
void CallSVC(std::uint32_t swi) override { ASSERT_MSG(false, "CallSVC({})", swi); }
|
||||||
|
|
||||||
void ExceptionRaised(u32 pc, Dynarmic::A32::Exception /*exception*/) override { ASSERT_MSG(false, "ExceptionRaised({:08x})", pc); }
|
void ExceptionRaised(u32 pc, Dynarmic::A32::Exception /*exception*/) override { ASSERT_MSG(false, "ExceptionRaised({:08x}) code = {:08x}", pc, MemoryReadCode(pc)); }
|
||||||
|
|
||||||
void AddTicks(std::uint64_t ticks) override {
|
void AddTicks(std::uint64_t ticks) override {
|
||||||
if (ticks > ticks_left) {
|
if (ticks > ticks_left) {
|
||||||
|
|
|
@ -35,10 +35,16 @@ u32 RandomFpcr() {
|
||||||
}
|
}
|
||||||
|
|
||||||
InstructionGenerator::InstructionGenerator(const char* format){
|
InstructionGenerator::InstructionGenerator(const char* format){
|
||||||
ASSERT(std::strlen(format) == 32);
|
const size_t format_len = std::strlen(format);
|
||||||
|
ASSERT(format_len == 16 || format_len == 32);
|
||||||
|
|
||||||
for (int i = 0; i < 32; i++) {
|
if (format_len == 16) {
|
||||||
const u32 bit = 1u << (31 - i);
|
// Begin with 16 zeros
|
||||||
|
mask |= 0xFFFF0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < format_len; i++) {
|
||||||
|
const u32 bit = 1u << (format_len - i - 1);
|
||||||
switch (format[i]) {
|
switch (format[i]) {
|
||||||
case '0':
|
case '0':
|
||||||
mask |= bit;
|
mask |= bit;
|
||||||
|
|
Loading…
Reference in a new issue