2018-03-02 15:31:11 +03:00
|
|
|
/*
|
|
|
|
* RISC-V GDB Server Stub
|
|
|
|
*
|
|
|
|
* Copyright (c) 2016-2017 Sagar Karandikar, sagark@eecs.berkeley.edu
|
|
|
|
*
|
|
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
|
|
* under the terms and conditions of the GNU General Public License,
|
|
|
|
* version 2 or later, as published by the Free Software Foundation.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope it will be useful, but WITHOUT
|
|
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
|
|
* more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License along with
|
|
|
|
* this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "qemu/osdep.h"
|
|
|
|
#include "exec/gdbstub.h"
|
2023-03-03 05:57:56 +03:00
|
|
|
#include "gdbstub/helpers.h"
|
2018-03-02 15:31:11 +03:00
|
|
|
#include "cpu.h"
|
|
|
|
|
2021-12-10 10:56:54 +03:00
|
|
|
struct TypeSize {
|
|
|
|
const char *gdb_type;
|
|
|
|
const char *id;
|
|
|
|
int size;
|
|
|
|
const char suffix;
|
|
|
|
};
|
|
|
|
|
|
|
|
static const struct TypeSize vec_lanes[] = {
|
|
|
|
/* quads */
|
|
|
|
{ "uint128", "quads", 128, 'q' },
|
|
|
|
/* 64 bit */
|
|
|
|
{ "uint64", "longs", 64, 'l' },
|
|
|
|
/* 32 bit */
|
|
|
|
{ "uint32", "words", 32, 'w' },
|
|
|
|
/* 16 bit */
|
|
|
|
{ "uint16", "shorts", 16, 's' },
|
|
|
|
/*
|
|
|
|
* TODO: currently there is no reliable way of telling
|
|
|
|
* if the remote gdb actually understands ieee_half so
|
|
|
|
* we don't expose it in the target description for now.
|
|
|
|
* { "ieee_half", 16, 'h', 'f' },
|
|
|
|
*/
|
|
|
|
/* bytes */
|
|
|
|
{ "uint8", "bytes", 8, 'b' },
|
|
|
|
};
|
|
|
|
|
2020-03-16 20:21:41 +03:00
|
|
|
int riscv_cpu_gdb_read_register(CPUState *cs, GByteArray *mem_buf, int n)
|
2018-03-02 15:31:11 +03:00
|
|
|
{
|
|
|
|
RISCVCPU *cpu = RISCV_CPU(cs);
|
|
|
|
CPURISCVState *env = &cpu->env;
|
2022-01-20 15:20:35 +03:00
|
|
|
target_ulong tmp;
|
2018-03-02 15:31:11 +03:00
|
|
|
|
|
|
|
if (n < 32) {
|
2022-01-20 15:20:35 +03:00
|
|
|
tmp = env->gpr[n];
|
2018-03-02 15:31:11 +03:00
|
|
|
} else if (n == 32) {
|
2022-01-20 15:20:35 +03:00
|
|
|
tmp = env->pc;
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (env->misa_mxl_max) {
|
|
|
|
case MXL_RV32:
|
|
|
|
return gdb_get_reg32(mem_buf, tmp);
|
|
|
|
case MXL_RV64:
|
2022-01-24 23:24:56 +03:00
|
|
|
case MXL_RV128:
|
2022-01-20 15:20:35 +03:00
|
|
|
return gdb_get_reg64(mem_buf, tmp);
|
|
|
|
default:
|
|
|
|
g_assert_not_reached();
|
2018-03-02 15:31:11 +03:00
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
int riscv_cpu_gdb_write_register(CPUState *cs, uint8_t *mem_buf, int n)
|
|
|
|
{
|
|
|
|
RISCVCPU *cpu = RISCV_CPU(cs);
|
|
|
|
CPURISCVState *env = &cpu->env;
|
2022-01-20 15:20:35 +03:00
|
|
|
int length = 0;
|
|
|
|
target_ulong tmp;
|
|
|
|
|
|
|
|
switch (env->misa_mxl_max) {
|
|
|
|
case MXL_RV32:
|
|
|
|
tmp = (int32_t)ldl_p(mem_buf);
|
|
|
|
length = 4;
|
|
|
|
break;
|
|
|
|
case MXL_RV64:
|
2022-01-24 23:24:56 +03:00
|
|
|
case MXL_RV128:
|
2022-01-20 15:20:35 +03:00
|
|
|
if (env->xl < MXL_RV64) {
|
|
|
|
tmp = (int32_t)ldq_p(mem_buf);
|
|
|
|
} else {
|
|
|
|
tmp = ldq_p(mem_buf);
|
|
|
|
}
|
|
|
|
length = 8;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
g_assert_not_reached();
|
|
|
|
}
|
|
|
|
if (n > 0 && n < 32) {
|
|
|
|
env->gpr[n] = tmp;
|
2018-03-02 15:31:11 +03:00
|
|
|
} else if (n == 32) {
|
2022-01-20 15:20:35 +03:00
|
|
|
env->pc = tmp;
|
2019-03-15 13:26:59 +03:00
|
|
|
}
|
2022-01-20 15:20:35 +03:00
|
|
|
|
|
|
|
return length;
|
2019-03-15 13:26:59 +03:00
|
|
|
}
|
|
|
|
|
2020-03-16 20:21:41 +03:00
|
|
|
static int riscv_gdb_get_fpu(CPURISCVState *env, GByteArray *buf, int n)
|
2019-03-15 13:26:59 +03:00
|
|
|
{
|
|
|
|
if (n < 32) {
|
2021-10-20 06:16:57 +03:00
|
|
|
if (env->misa_ext & RVD) {
|
2020-03-16 20:21:41 +03:00
|
|
|
return gdb_get_reg64(buf, env->fpr[n]);
|
2020-01-29 02:32:16 +03:00
|
|
|
}
|
2021-10-20 06:16:57 +03:00
|
|
|
if (env->misa_ext & RVF) {
|
2020-03-16 20:21:41 +03:00
|
|
|
return gdb_get_reg32(buf, env->fpr[n]);
|
2020-01-29 02:32:16 +03:00
|
|
|
}
|
2019-03-15 13:26:59 +03:00
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int riscv_gdb_set_fpu(CPURISCVState *env, uint8_t *mem_buf, int n)
|
|
|
|
{
|
|
|
|
if (n < 32) {
|
|
|
|
env->fpr[n] = ldq_p(mem_buf); /* always 64-bit */
|
2018-03-02 15:31:11 +03:00
|
|
|
return sizeof(uint64_t);
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
2019-03-15 13:26:59 +03:00
|
|
|
|
2021-12-10 10:56:54 +03:00
|
|
|
static int riscv_gdb_get_vector(CPURISCVState *env, GByteArray *buf, int n)
|
|
|
|
{
|
|
|
|
uint16_t vlenb = env_archcpu(env)->cfg.vlen >> 3;
|
|
|
|
if (n < 32) {
|
|
|
|
int i;
|
|
|
|
int cnt = 0;
|
|
|
|
for (i = 0; i < vlenb; i += 8) {
|
|
|
|
cnt += gdb_get_reg64(buf,
|
|
|
|
env->vreg[(n * vlenb + i) / 8]);
|
|
|
|
}
|
|
|
|
return cnt;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int riscv_gdb_set_vector(CPURISCVState *env, uint8_t *mem_buf, int n)
|
|
|
|
{
|
|
|
|
uint16_t vlenb = env_archcpu(env)->cfg.vlen >> 3;
|
|
|
|
if (n < 32) {
|
|
|
|
int i;
|
|
|
|
for (i = 0; i < vlenb; i += 8) {
|
|
|
|
env->vreg[(n * vlenb + i) / 8] = ldq_p(mem_buf + i);
|
|
|
|
}
|
|
|
|
return vlenb;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2020-03-16 20:21:41 +03:00
|
|
|
static int riscv_gdb_get_csr(CPURISCVState *env, GByteArray *buf, int n)
|
2019-03-15 13:26:59 +03:00
|
|
|
{
|
2021-01-16 08:41:22 +03:00
|
|
|
if (n < CSR_TABLE_SIZE) {
|
2019-03-15 13:26:59 +03:00
|
|
|
target_ulong val = 0;
|
|
|
|
int result;
|
|
|
|
|
2021-01-16 08:41:22 +03:00
|
|
|
result = riscv_csrrw_debug(env, n, &val, 0, 0);
|
2021-04-01 18:18:07 +03:00
|
|
|
if (result == RISCV_EXCP_NONE) {
|
2020-03-16 20:21:41 +03:00
|
|
|
return gdb_get_regl(buf, val);
|
2019-03-15 13:26:59 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int riscv_gdb_set_csr(CPURISCVState *env, uint8_t *mem_buf, int n)
|
|
|
|
{
|
2021-01-16 08:41:22 +03:00
|
|
|
if (n < CSR_TABLE_SIZE) {
|
2019-03-15 13:26:59 +03:00
|
|
|
target_ulong val = ldtul_p(mem_buf);
|
|
|
|
int result;
|
|
|
|
|
2021-01-16 08:41:22 +03:00
|
|
|
result = riscv_csrrw_debug(env, n, NULL, val, -1);
|
2021-04-01 18:18:07 +03:00
|
|
|
if (result == RISCV_EXCP_NONE) {
|
2019-03-15 13:26:59 +03:00
|
|
|
return sizeof(target_ulong);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2020-03-16 20:21:41 +03:00
|
|
|
static int riscv_gdb_get_virtual(CPURISCVState *cs, GByteArray *buf, int n)
|
2019-10-14 18:45:28 +03:00
|
|
|
{
|
|
|
|
if (n == 0) {
|
|
|
|
#ifdef CONFIG_USER_ONLY
|
2020-03-16 20:21:41 +03:00
|
|
|
return gdb_get_regl(buf, 0);
|
2019-10-14 18:45:28 +03:00
|
|
|
#else
|
2020-03-16 20:21:41 +03:00
|
|
|
return gdb_get_regl(buf, cs->priv);
|
2019-10-14 18:45:28 +03:00
|
|
|
#endif
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int riscv_gdb_set_virtual(CPURISCVState *cs, uint8_t *mem_buf, int n)
|
|
|
|
{
|
2019-10-14 18:45:29 +03:00
|
|
|
if (n == 0) {
|
|
|
|
#ifndef CONFIG_USER_ONLY
|
|
|
|
cs->priv = ldtul_p(mem_buf) & 0x3;
|
|
|
|
if (cs->priv == PRV_H) {
|
|
|
|
cs->priv = PRV_S;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
return sizeof(target_ulong);
|
|
|
|
}
|
2019-10-14 18:45:28 +03:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2021-01-16 08:41:22 +03:00
|
|
|
static int riscv_gen_dynamic_csr_xml(CPUState *cs, int base_reg)
|
|
|
|
{
|
|
|
|
RISCVCPU *cpu = RISCV_CPU(cs);
|
|
|
|
CPURISCVState *env = &cpu->env;
|
|
|
|
GString *s = g_string_new(NULL);
|
|
|
|
riscv_csr_predicate_fn predicate;
|
2021-10-20 06:16:58 +03:00
|
|
|
int bitsize = 16 << env->misa_mxl_max;
|
2021-01-16 08:41:22 +03:00
|
|
|
int i;
|
|
|
|
|
2023-02-28 13:40:27 +03:00
|
|
|
#if !defined(CONFIG_USER_ONLY)
|
|
|
|
env->debugger = true;
|
|
|
|
#endif
|
|
|
|
|
2022-01-07 00:00:57 +03:00
|
|
|
/* Until gdb knows about 128-bit registers */
|
|
|
|
if (bitsize > 64) {
|
|
|
|
bitsize = 64;
|
|
|
|
}
|
|
|
|
|
2021-01-16 08:41:22 +03:00
|
|
|
g_string_printf(s, "<?xml version=\"1.0\"?>");
|
|
|
|
g_string_append_printf(s, "<!DOCTYPE feature SYSTEM \"gdb-target.dtd\">");
|
|
|
|
g_string_append_printf(s, "<feature name=\"org.gnu.gdb.riscv.csr\">");
|
|
|
|
|
|
|
|
for (i = 0; i < CSR_TABLE_SIZE; i++) {
|
2023-02-28 13:40:17 +03:00
|
|
|
if (env->priv_ver < csr_ops[i].min_priv_ver) {
|
|
|
|
continue;
|
|
|
|
}
|
2021-01-16 08:41:22 +03:00
|
|
|
predicate = csr_ops[i].predicate;
|
2021-06-15 11:51:33 +03:00
|
|
|
if (predicate && (predicate(env, i) == RISCV_EXCP_NONE)) {
|
2021-01-16 08:41:22 +03:00
|
|
|
if (csr_ops[i].name) {
|
|
|
|
g_string_append_printf(s, "<reg name=\"%s\"", csr_ops[i].name);
|
|
|
|
} else {
|
|
|
|
g_string_append_printf(s, "<reg name=\"csr%03x\"", i);
|
|
|
|
}
|
|
|
|
g_string_append_printf(s, " bitsize=\"%d\"", bitsize);
|
|
|
|
g_string_append_printf(s, " regnum=\"%d\"/>", base_reg + i);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
g_string_append_printf(s, "</feature>");
|
|
|
|
|
|
|
|
cpu->dyn_csr_xml = g_string_free(s, false);
|
2023-02-28 13:40:27 +03:00
|
|
|
|
|
|
|
#if !defined(CONFIG_USER_ONLY)
|
|
|
|
env->debugger = false;
|
|
|
|
#endif
|
|
|
|
|
2021-01-16 08:41:22 +03:00
|
|
|
return CSR_TABLE_SIZE;
|
|
|
|
}
|
|
|
|
|
2021-12-10 10:56:54 +03:00
|
|
|
static int ricsv_gen_dynamic_vector_xml(CPUState *cs, int base_reg)
|
|
|
|
{
|
|
|
|
RISCVCPU *cpu = RISCV_CPU(cs);
|
|
|
|
GString *s = g_string_new(NULL);
|
|
|
|
g_autoptr(GString) ts = g_string_new("");
|
|
|
|
int reg_width = cpu->cfg.vlen;
|
|
|
|
int num_regs = 0;
|
|
|
|
int i;
|
|
|
|
|
|
|
|
g_string_printf(s, "<?xml version=\"1.0\"?>");
|
|
|
|
g_string_append_printf(s, "<!DOCTYPE target SYSTEM \"gdb-target.dtd\">");
|
|
|
|
g_string_append_printf(s, "<feature name=\"org.gnu.gdb.riscv.vector\">");
|
|
|
|
|
|
|
|
/* First define types and totals in a whole VL */
|
|
|
|
for (i = 0; i < ARRAY_SIZE(vec_lanes); i++) {
|
|
|
|
int count = reg_width / vec_lanes[i].size;
|
|
|
|
g_string_printf(ts, "%s", vec_lanes[i].id);
|
|
|
|
g_string_append_printf(s,
|
|
|
|
"<vector id=\"%s\" type=\"%s\" count=\"%d\"/>",
|
|
|
|
ts->str, vec_lanes[i].gdb_type, count);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Define unions */
|
|
|
|
g_string_append_printf(s, "<union id=\"riscv_vector\">");
|
|
|
|
for (i = 0; i < ARRAY_SIZE(vec_lanes); i++) {
|
|
|
|
g_string_append_printf(s, "<field name=\"%c\" type=\"%s\"/>",
|
|
|
|
vec_lanes[i].suffix,
|
|
|
|
vec_lanes[i].id);
|
|
|
|
}
|
|
|
|
g_string_append(s, "</union>");
|
|
|
|
|
|
|
|
/* Define vector registers */
|
|
|
|
for (i = 0; i < 32; i++) {
|
|
|
|
g_string_append_printf(s,
|
|
|
|
"<reg name=\"v%d\" bitsize=\"%d\""
|
|
|
|
" regnum=\"%d\" group=\"vector\""
|
|
|
|
" type=\"riscv_vector\"/>",
|
|
|
|
i, reg_width, base_reg++);
|
|
|
|
num_regs++;
|
|
|
|
}
|
|
|
|
|
|
|
|
g_string_append_printf(s, "</feature>");
|
|
|
|
|
|
|
|
cpu->dyn_vreg_xml = g_string_free(s, false);
|
|
|
|
return num_regs;
|
|
|
|
}
|
|
|
|
|
2019-03-15 13:26:59 +03:00
|
|
|
void riscv_cpu_register_gdb_regs_for_features(CPUState *cs)
|
|
|
|
{
|
|
|
|
RISCVCPU *cpu = RISCV_CPU(cs);
|
|
|
|
CPURISCVState *env = &cpu->env;
|
2021-10-20 06:16:57 +03:00
|
|
|
if (env->misa_ext & RVD) {
|
2020-01-29 02:32:16 +03:00
|
|
|
gdb_register_coprocessor(cs, riscv_gdb_get_fpu, riscv_gdb_set_fpu,
|
target/riscv: remove fflags, frm, and fcsr from riscv-*-fpu.xml
While testing some changes to GDB's handling for the RISC-V registers
fcsr, fflags, and frm, I spotted that QEMU includes these registers
twice in the target description it sends to GDB, once in the fpu
feature, and once in the csr feature.
Right now things basically work OK, QEMU maps these registers onto two
different register numbers, e.g. fcsr maps to both 68 and 73, and GDB
can use either of these to access the register.
However, GDB's target descriptions don't really work this way, each
register should appear just once in a target description, mapping the
register name onto the number GDB should use when accessing the
register on the target. Duplicate register names actually result in
duplicate registers on the GDB side, however, as the registers have
the same name, the user can only access one of these registers.
Currently GDB has a hack in place, specifically for RISC-V, to spot
the duplicate copies of these three registers, and hide them from the
user, ensuring the user only ever sees a single copy of each.
In this commit I propose fixing this issue on the QEMU side, and in
the process, simplify the fpu register handling a little.
I think we should, remove fflags, frm, and fcsr from the two (32-bit
and 64-bit) fpu feature xml files. These files will only contain the
32 core floating point register f0 to f31. The fflags, frm, and fcsr
registers will continue to be advertised in the csr feature as they
currently are.
With that change made, I will simplify riscv_gdb_get_fpu and
riscv_gdb_set_fpu, removing the extra handling for the 3 status
registers.
Signed-off-by: Andrew Burgess <aburgess@redhat.com>
Reviewed-by: Alistair Francis <alistair.francis@wdc.com>
Message-Id: <0fbf2a5b12e3210ff3867d5cf7022b3f3462c9c8.1661934573.git.aburgess@redhat.com>
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
2022-08-31 11:41:22 +03:00
|
|
|
32, "riscv-64bit-fpu.xml", 0);
|
2021-10-20 06:16:57 +03:00
|
|
|
} else if (env->misa_ext & RVF) {
|
2019-03-15 13:26:59 +03:00
|
|
|
gdb_register_coprocessor(cs, riscv_gdb_get_fpu, riscv_gdb_set_fpu,
|
target/riscv: remove fflags, frm, and fcsr from riscv-*-fpu.xml
While testing some changes to GDB's handling for the RISC-V registers
fcsr, fflags, and frm, I spotted that QEMU includes these registers
twice in the target description it sends to GDB, once in the fpu
feature, and once in the csr feature.
Right now things basically work OK, QEMU maps these registers onto two
different register numbers, e.g. fcsr maps to both 68 and 73, and GDB
can use either of these to access the register.
However, GDB's target descriptions don't really work this way, each
register should appear just once in a target description, mapping the
register name onto the number GDB should use when accessing the
register on the target. Duplicate register names actually result in
duplicate registers on the GDB side, however, as the registers have
the same name, the user can only access one of these registers.
Currently GDB has a hack in place, specifically for RISC-V, to spot
the duplicate copies of these three registers, and hide them from the
user, ensuring the user only ever sees a single copy of each.
In this commit I propose fixing this issue on the QEMU side, and in
the process, simplify the fpu register handling a little.
I think we should, remove fflags, frm, and fcsr from the two (32-bit
and 64-bit) fpu feature xml files. These files will only contain the
32 core floating point register f0 to f31. The fflags, frm, and fcsr
registers will continue to be advertised in the csr feature as they
currently are.
With that change made, I will simplify riscv_gdb_get_fpu and
riscv_gdb_set_fpu, removing the extra handling for the 3 status
registers.
Signed-off-by: Andrew Burgess <aburgess@redhat.com>
Reviewed-by: Alistair Francis <alistair.francis@wdc.com>
Message-Id: <0fbf2a5b12e3210ff3867d5cf7022b3f3462c9c8.1661934573.git.aburgess@redhat.com>
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
2022-08-31 11:41:22 +03:00
|
|
|
32, "riscv-32bit-fpu.xml", 0);
|
2019-03-15 13:26:59 +03:00
|
|
|
}
|
2021-12-10 10:56:54 +03:00
|
|
|
if (env->misa_ext & RVV) {
|
2023-02-28 13:40:20 +03:00
|
|
|
int base_reg = cs->gdb_num_regs;
|
2021-12-10 10:56:54 +03:00
|
|
|
gdb_register_coprocessor(cs, riscv_gdb_get_vector, riscv_gdb_set_vector,
|
2023-02-28 13:40:20 +03:00
|
|
|
ricsv_gen_dynamic_vector_xml(cs, base_reg),
|
2021-12-10 10:56:54 +03:00
|
|
|
"riscv-vector.xml", 0);
|
|
|
|
}
|
2022-01-20 15:20:35 +03:00
|
|
|
switch (env->misa_mxl_max) {
|
|
|
|
case MXL_RV32:
|
|
|
|
gdb_register_coprocessor(cs, riscv_gdb_get_virtual,
|
|
|
|
riscv_gdb_set_virtual,
|
|
|
|
1, "riscv-32bit-virtual.xml", 0);
|
|
|
|
break;
|
|
|
|
case MXL_RV64:
|
2022-01-24 23:24:56 +03:00
|
|
|
case MXL_RV128:
|
2022-01-20 15:20:35 +03:00
|
|
|
gdb_register_coprocessor(cs, riscv_gdb_get_virtual,
|
|
|
|
riscv_gdb_set_virtual,
|
|
|
|
1, "riscv-64bit-virtual.xml", 0);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
g_assert_not_reached();
|
|
|
|
}
|
2021-01-16 08:41:22 +03:00
|
|
|
|
2023-02-28 13:40:21 +03:00
|
|
|
if (cpu->cfg.ext_icsr) {
|
|
|
|
int base_reg = cs->gdb_num_regs;
|
|
|
|
gdb_register_coprocessor(cs, riscv_gdb_get_csr, riscv_gdb_set_csr,
|
|
|
|
riscv_gen_dynamic_csr_xml(cs, base_reg),
|
|
|
|
"riscv-csr.xml", 0);
|
|
|
|
}
|
2019-03-15 13:26:59 +03:00
|
|
|
}
|