LD DE, 0000 + ADD HL, DE = 10+4 = 14 тактов
LD A,H + OR L = 5+4 = 9 тактов
Но если DE загружать вне цикла, то покатит.
Вид для печати
LD DE, 0000 + ADD HL, DE = 10+4 = 14 тактов
LD A,H + OR L = 5+4 = 9 тактов
Но если DE загружать вне цикла, то покатит.
LXI 10 тактов. DAD тоже. Итого 20.
---------- Post added at 17:37 ---------- Previous post was at 17:36 ----------
Давай будем разговаривать в мнемониках i8080 :)
http://www.emuverse.ru/wiki/Intel_80...B0%D0%BD%D0%B4
DAD = 4 такта
---------- Post added at 15:39 ---------- Previous post was at 15:38 ----------
А в Intel_8080_ASM_Lang_Manual.pdf написано 10
Я тут просматривал код:
И подумал - а не стоит ли переменные засовывать прямо в код, для циклов? Выделенную строчку заменить на две:Код:; for(x=0; x<64; x+=16) {
xor a
ld (main_x), a
l10:
ld d, 64
ld a, (main_x)
call l_uchar
or a
jp z, l11
jp l12
l13:
ld a, (main_x)
add 16
ld (main_x), a
jp l10
l12:
; }
jp l13
l11:
main_x_var:
ld a,0
нолик, соответственно, место где хранится переменная, она же теперь часть кода. Соответственно, обращаться к переменной теперь не ld a, (main_x), а ld a, (main_x_var+1). Код на байт короче, байт экономится на глобальной переменной, 6 тактов на каждой итерации цикла. Правда 2 минуса - нужно следить за обращением к x, он теперь на новом месте, и такой код нельзя зашить в ROM.
Хм, сделаю и такой режим :) Но когда переменные лежат одним блоком, легко реализовать рекурсию. Просто этот блок переменных копировать в стек.
Выхожу на финишную прямую с кодогенератором. Сделал операции < > <= >= == != для uchar. Сделал #define #undef (как полагается с параметрами).
В этом коде еще убрать дублирующиеся присваивания переменных (ld a, 1 ld (print_x), a ld a, 1) или (ld (atoi_a), hl, ld hl, (atoi_a)) и т.д. Эта уборка будет отключатся словом volatile. Объединить XOR A и LD (HL), A, например (xor a ld hl, (clrscr_dest) ld (hl), a). И убрать лишние переходы, например jp $+3 или jp на другой jp.Код:print_return ds 0
print_x ds 1
print_y ds 1
print_text ds 2
print_dest ds 2
print:
push bc
ld (print_text), hl
; 10 dest = (uchar*)(0xE1D5 + x + y*78);
ld hl, (print_x)
ld h, 0
ld de, 57813
add hl, de
push hl
ld d, 78
ld a, (print_y)
call mul_uchar
pop de
add hl, de
ld bc, hl
; 11 while(*text) { *dest = *text; dest++; text++; }
l2:
ld hl, (print_text)
ld a, (hl)
or a
jp z, l3
; 11 *dest = *text; dest++; text++; }
ld hl, (print_text)
ld a, (hl)
ld (bc), a
; 11 dest++; text++; }
inc bc
; 11 text++; }
ld hl, (print_text)
inc hl
ld (print_text), hl
jp l2
l3:
l1:
pop bc
ret
;----------------------------------
clrscr_return ds 0
clrscr_dest ds 2
clrscr_c ds 2
clrscr:
push bc
; 17 dest = (uchar*)0xE1D0-1;
ld hl, 57807
ld (clrscr_dest), hl
; 18 c = 78*25;
ld bc, 1950
; 19 do *++dest = 0; while(c--);
l5:
; 19 *++dest = 0; while(c--);
ld hl, (clrscr_dest)
inc hl
ld (clrscr_dest), hl
xor a
ld hl, (clrscr_dest)
ld (hl), a
dec bc
ld a, b
or c
jp nz, l5
l4:
pop bc
ret
;----------------------------------
get:
; 23 return 23;
ld hl, 23
jp l6
l6:
ret
;----------------------------------
atoi_return ds 0
atoi_str1 ds 2
atoi_a ds 2
atoi_str ds 2
atoi_c ds 1
atoi:
push bc
ld (atoi_a), hl
; 31 str = str1;
ld hl, (atoi_str1)
ld bc, hl
; 32 do {
l8:
; 33 c = ((uchar)a & 15) + 48;
ld a, (atoi_a)
and 15
add 48
ld (atoi_c), a
; 34 if('9' < c) c += 7;
ld a, (atoi_c)
cp 57
jp c, l10
; 34 c += 7;
ld a, (atoi_c)
add 7
ld (atoi_c), a
l10:
; 35 *str = c; str++;
ld a, (atoi_c)
ld (bc), a
; 35 str++;
inc bc
; 36 a = a >> 4;
ld de, 4
ld hl, (atoi_a)
call shr_ushort
ld (atoi_a), hl
ld hl, (atoi_a)
ld a, l
or h
jp nz, l8
; 38 *str = 0;
xor a
ld (bc), a
l7:
pop bc
ret
;----------------------------------
main_return ds 0
main_i ds 1
main_x ds 1
main_buf ds 32
main:
push bc
; 47 clrscr();
call clrscr
; 48 print(0, 0, "HELLO");
xor a
ld (print_x), a
xor a
ld (print_y), a
ld hl, string0
call print
; 49 atoi(buf, 0x12AB);
ld hl, main_buf
ld (atoi_str1), hl
ld hl, 4779
call atoi
; 51 print(1, 1, buf);
ld a, 1
ld (print_x), a
ld a, 1
ld (print_y), a
ld hl, main_buf
call print
; 53 i=5;
ld b, 5
; 55 for(x=0; x<64; x+=XYZ(12)) {
ld c, 0
l12:
ld a, c
cp 64
jp nc, l13
l12:
; 56 print(x, i, "12345678.123");
ld a, c
ld (print_x), a
ld a, b
ld (print_y), a
ld hl, string1
call print
ld a, c
add 14
ld c, a
jp l12
l13:
; 59 while(1) {
l15:
; 60 if(getch() == '1') clrscr();
call getch
cp 49
jp nz, l18
; 60 clrscr();
call clrscr
l18:
jp l15
l16:
l11:
pop bc
ret
1888 строк 54 Кб.
---------- Post added at 03:23 ---------- Previous post was at 02:47 ----------
Мне написали, что есть компиляторы Си для 8080.
https://github.com/begoon/smallc-scc3, https://github.com/begoon/smallc-85
и работают они так
Код:main() {
static char a;
for (a = '\0'; a < '\10'; ++a) {
a = a + '\1';
}
}
(специально использовал char везде) генерит адъ типа:
;main() {
main:
; static char a;
dseg
?2: ds 1
cseg
; for (a = '\0'; a < '\10'; ++a) {
lxi h,?2
push h
lxi h,0
pop d
call ?pchar
?3:
lxi h,?2
call ?gchar
push h
lxi h,12592
pop d
call ?lt
mov a,h
ora l
jnz ?5
jmp ?6
?4:
lxi h,?2
push h
call ?gchar
inx h
pop d
call ?pchar
jmp ?3
?5:
; a = a + '\1';
lxi h,?2
push h
lxi h,?2
call ?gchar
push h
lxi h,49
pop d
dad d
pop d
call ?pchar
; }
jmp ?4
?6:
;}
?1:
ret
мой
main:
push bc
; 3 for (a = 0; a < 10; ++a) {
ld b, 0
l1:
ld a, b
cp 10
jp nc, l2
l1:
; 4 a = a + 1;
ld a, b
inc a
ld b, a
inc b
jp l1
l2:
l0:
pop bc
ret
Ну далеко не всегда нужно все переменные копировать в стек. В идеале нужно вести время жизни переменных. Часть переменных уже не используется, часть ещё не присвоены, часть будут безусловно проинициализированы после вызова, и текущее значение не нужно.
А это зачем?
ld hl, (clrscr_dest)
ld (hl), a
Регистр а можно прямо поместить по нужному адресу.
Кстати, 0xE1D5 + x можно ещё оптимальнее
Вместо
ld hl, (print_x)
ld h, 0
ld de, 57813 // 0xE1D5
add hl, de
Сделать
ld hl, (print_x)
ld de, 0x0СD5 // d=E1-D5=0C
ld h, е // На байт короче, на 3 такта быстрее
add hl, de
А вот здесь у тебя неверный код, вроде
while(c--);
dec bc
ld a, b
or c
jp nz, l5
У тебя код не постдекрементный, а преддекрементный. Т.е. для случая while(--c), а не while(c--).
Переменные на стеке нужны для реентабельности процедур. Это не только рекурсия, но и второе в хождение в ту же процедуру из обработчика перерывания. Соответственно, в случае работы системы с прерываниями, копировать глобальные переменные на стек бессмысленно - остается вероятность что по прерыванию будет повторное вхождение пока переменные еще не скопированы. Ну и если будет такое копирование, всякий выигрыш теряется (что про времени выполнения, что по размеру кода). Тогда уж сразу надо делать на стеке.
Насчёт реентабельности по прерываниям - да, абсолютно верно, такие процедуры нереентабельные. А вот с "Ну и всякий выигрыш теряется (что про времени выполнения, что по размеру кода)" я не согласен. Для 8080 (не Z80) операции со стеком весьма и весьма мудоёмкие, сжирающие как минимум одну из трёх регистровых пар (HL), которая весьма незаменима, а это постоянные прологи из двух-пяти команд для каждого обращения к стеку. Выигрыш весьма существенный. Особенно в тех случаях, когда возможен анализ кода, есть ли возможность рекурсии при вызове некой функции.
-
Проблема нереентабельности по прерываниям, как по мне, несущественная. Обработчик обычно пишется на ассемблере, если какие-то сишные функции и будут вызываться - то обычно это не те, что выполняются вне прерывания. Этот факт нужно отразить в документации, и принять его "как есть".
-
Более серьёзная проблема, как по мне - это передача ссылки на локальную переменную в некую функцию. Если создаются копии локальных переменных в стеке, то ссылка на локальную переменную "повиснет". А если ещё добавить, что вызываемая функция эту ссылку может сохранить в глобальной переменной, и другая вызываемая функция (из базовой функции) эту ссылку может оттуда поднять... Тут весьма сложный момент. И врядли он будет сделан в соответствии со стандартами языка. Я не хочу сейчас грузить vinxru этой проблемой. Он и так молодец.
В ячейке clrscr_dest хранится адрес, который предыдущей строкой увеличивается.
Нет на 8080 команды ld ((clrscr_dest)), A
А вот на PDP-11 есть.
---------- Post added at 14:31 ---------- Previous post was at 14:30 ----------
Да, я пока одну версию операторов написал. Сегодня поправлю, а то забуду.
---------- Post added at 14:34 ---------- Previous post was at 14:31 ----------
Чисто арифметически выигрыш есть. Копируем мы переменную один раз на входе, один на выходе. Причем, копирование всех переменных можно делать за один присест.
А обращаться к переменной мы будем много раз, может быть что 1000 раз.
---------- Post added at 14:35 ---------- Previous post was at 14:34 ----------
Можно продублировать функцию вызываемую из прерывания и из основного кода.
---------- Post added at 14:37 ---------- Previous post was at 14:35 ----------
Это компромисс между
1) Программой, которая умеет рекурсию.
2) И программой, которая работает в 3 раза быстрее.
Извиняюсь, просмотрел, что тут двойной доступ.
Догадываюсь, что код сильно усложнится. Прединкремент/преддекремент работает быстрее, и код короче. Конкретно здесь, поскольку переменная с в теле не используется, его стОит оставить, только увеличить на единицу BC (или сразу загрузить в него увеличенную на 1 константу, как в этом случае). Впрочем, анализ циклов сложное дело, и я понимаю, что сейчас оно не на времени.Цитата:
Да, я пока одну версию операторов написал. Сегодня поправлю, а то забуду.
Безусловно. Кому надо - продублирует.
Полностью поддерживаю. Компилятор прежде всего будет использоваться любителями старых компов, типа Львова, а там абсолютный приоритет - быстродействие.
Единственно замечу, что это проблема не только рекурсии. В том примере, что я привёл, это возможно при вызове из функции двух функций, которые никак рекурсию не производят. Просто отразить в документации - если хочешь иметь корректный указатель на лок.перем. между вызовами разных функций - используй указатель на static или volatile переменную.
Так надо ввести опцию компилятора: все переменные статические - да/нет. А компилятор делать классически, с поддержкой локальных переменных в стеке. Кому не нравится глобальная переменная, поставит перед декларацией register.
---------- Post added at 19:26 ---------- Previous post was at 19:23 ----------
И в тексте программы учитывать:
#pragma static_variable on
#pragma static_variable off
Ээээ... ничего что стандарт C99 позволяет указатели на регистровые локальные переменные? И это как раз такой случай, что она должна располагаться в стеке, если конечно компилятор не соптимизирует обращение к переменной, и есть гарантия, что доступа к адресу не будет.
Так что вопрос несколько сложнее, и просто register это не решает.
Здесь планируется несколько более, чем просто статические переменные. Их можно использовать как и автоматические, для рекурсии. Можно сказать, что локальные переменные просто кешируются в памяти по конкретным адресам.
У меня нельзя будет получить адрес регистровой переменной. Как раз сейчас сообщений об ошибках придумывал.
case pVar8: p.logicError_("Нельзя получить адрес переменной преобразованной из 8 бит"); break; // &(ushort)a;
case pA: case pHl: case pStack: p.logicError_("Нельзя получить адрес временного значения"); break;
case pB: case pC: case pBC: p.logicError_("Нельзя получить адрес регистровой переменной"); break;
case pConst: p.logicError_("Нельзя получить адрес константы"); break;
---------- Post added at 19:42 ---------- Previous post was at 19:40 ----------
Кстати первая ошибка - это косяк оптимизации, потом сделаю обход.
Когда мы преобразовываем значение
char a;
short b = (short)a + 0x1234;
Сразу А не преобразуется. Просто функции чтения забивают старшие 8 бит нулями. Это работает везде кроме
short* b = &(short)a;
Оно и так не должно выполняться. А если выполнится, то будет идентично
short* b = (short*)&a;
---------- Post added at 19:43 ---------- Previous post was at 19:42 ----------
Чё я торможу. Это ошибка "Нельзя получить адрес временного значения"
---------- Post added at 20:34 ---------- Previous post was at 19:43 ----------
Сейчас мой мозг вытечет через ухо
Есть абстрактные команды
moNot, moNeg, moAddr, moDeAddr, moPostInc, moPostDec, moInc, moDec
oDiv, oMod, oMul, oAdd, oSub, oShl, oShr, oL, oG, oLE, oGE, oE, oNE, oAnd, oXor, oOr, oLAnd, oLOr, oIf, oSet, oSAdd, oSSub, oSMul, oSDiv, oSMod, oSShl, oSShr, oSAnd, oSXor, oSOr
И такой набор команд на каждый тип данных char, short, long, ulong.
У них несколько значений на входе. Причем значения могут быть:
pConst - это просто число
pConstRef - адрес в памяти
pVar - какая то переменная
pVar8 - переменная, причем используются только 8 нижних бит
pRef - переменная содержащая адрес в памяти
pArray - переменная, но компилятор должен подставлять её адрес в программу вместо значения.
pHl, pA, pB, pC, pBC - регистры
pStack - верхушка стека
pBCRef, pHLRef - в регистрах лежит адрес значения в памяти.
И при этом регистры A, DE, HL используются для временного хранения переменных и их иногда можно использовать, а иногда нельзя.
И при этом, команды должны сваливать результат в
pConstRef - адрес в памяти
pVar - какая то переменная
pRef - переменная содержащая адрес в памяти
pHl, pA, pB, pC, pBC - регистры
pStack - верхушка стека
pBCRef, pHLRef - в регистрах лежит адрес значения в памяти.
---------- Post added at 20:38 ---------- Previous post was at 20:34 ----------
Ой, да. Команды при этом еще объединяются и упрощаются. Например сравнение и переход.
---------- Post added at 21:27 ---------- Previous post was at 20:38 ----------
А чем отличается RAL от ADС A, кроме изменяемых флагов?
У ADC A справа нулевой бит добавляется, а RAL - берёт флаг переноса.
У разве не у ADD нулевой бит добавляется?
Фу-ты, попутал. Значит ничем :)
Команды RAL вообще мне не пригодились
16 битный сдвиг DAD H
8 битный сдвиг ADD A
8 бит в другую сторону CP A + RAR
16 бит вправо самая сложная
MOV A, H/B
CP A
RAR
MOV H, A
MOV A, L/C
RAR
MOV L, A
И еще я что то не могу сообразить, как сравнивать знаковые числа. Беззнаковые просто, по флагам Z и C. А знаковые я только придумал
(n1+80h) < (n2+80h)
MOV A, N1
XRI 0x80
MOV D, A
MOV A, N2
XRI 0x80
CP D
А дальше так же по флагам C, Z
Но конструкция получается тормозная и большая. Такую не жалко в подпрограмму вынести, что бы зря размер кода не увеличивать.
А если просто вычесть и знак результата проверить?
MOV A,N1
MOV B,N2
SUB B
JM less
Ааа... спать надо больше. Точно ведь. А мне лень даже в яндекс вбить слово.
---------- Post added at 00:39 ---------- Previous post was at 00:33 ----------
У меня получилось, что myFunction(1, myFuction(2, 3)) некорректно работает
mvi a, 1
sta myFunction_paramA
mvi a, 2
sta myFunction_paramA
mvi a, 3
call myFunction
call myFunction
Но это можно обойти
---------- Post added at 00:40 ---------- Previous post was at 00:39 ----------
В среду дам первую версию. Эта еще сырая очень.
2100 строк 66 Кб
И всё-таки по причинам реентерабельности, стоит подумать о размещении локальных переменных на стеке. Дело здесь не только в interrupt функциях, для которыз кстати надо ещё PUSHALL делать. Просто, если компилятор будет использоваться для написания оконных интерфейсов, почти неизбежны локальные стеки и переключения контекстов.
Возможно, Вы подумаете о чём-нибудь типа
#pragma recursion:ON/OFF
Также пожелание поддерживать несколько calling conventions, по крайей мере extern "C" / _stdcall, _pascal, _memblock (это-Ваш вариант) и _fast (передача до 2х параметров в регистрах)
Сделаю рекурсию. Добавлю модификаторы _static, _stack. Пока не до этого, пока у меня даже комментарии не поддерживаются. :)
---------- Post added at 01:22 ---------- Previous post was at 00:52 ----------
Прикольно. Написал
x=32; y=15;
while(1) {
i=getch();
print(x, y, "+");
if(i==0x19) { if(y>0) --y; } else
if(i==0x08) { if(x>0) --x; } else
if(i==0x1A) { if(y<24) ++y; } else
if(i==0x18) { if(x<63) ++x; }
print(x, y, "O");
}
сижу рисую. :) Теперь жизнь станет проще, а программы круче.
---------- Post added at 01:36 ---------- Previous post was at 01:22 ----------
Код:Программа
struct File {
ushort some1, size;
uchar name[11];
uchar some2;
};
File files[24];
void main() {
register uchar i;
File* f;
clrscr();
for(f=files, i=0; i<24; ++i, ++f)
itoa(f->name, i);
for(i=0; i<16; ++i)
print(0, i, files[i].name);
while(1);
}
files ds 384
main_f ds 2
main:
push bc
; 49 clrscr();
call clrscr
; 51 for(f=files, i=0; i<24; ++i, ++f)
ld hl, files
ld (main_f), hl
ld b, 0
l12:
ld a, b
cp 24
jp nc, l13
; 52 atoi(f->name, i);
ld hl, (main_f)
inc hl
inc hl
inc hl
inc hl
ld (atoi_str1), hl
ld l, b
ld h, 0
call atoi
inc b
ld hl, (main_f)
ld de, 16
add hl, de
ld (main_f), hl
jp l12
l13:
; 54 for(i=0; i<16; ++i)
ld b, 0
l15:
ld a, b
cp 16
jp nc, l16
; 55 print(0, i, files[i].name);
xor a
ld (print_x), a
ld a, b
ld (print_y), a
ld l, b
ld h, 0
add hl, hl
add hl, hl
add hl, hl
add hl, hl
ld de, files
add hl, de
inc hl
inc hl
inc hl
inc hl
call print
inc b
jp l15
l16:
; 57 while(1);
l18:
jp l18
l19:
l11:
pop bc
ret
Если сравнивать только положительные или только отрицательные - то да.
Например.
10-8 = 2. бит 7 нулевой. 10>8
(-1)-(-2)=255-254=1. бит 7 нулевой. -1>-2
А вот если одни с другими...
127 - (-2) = 127 - 254 = -127 = 0х81 бит 7 ненулевой. 127<-2. Но это неверно!
Тут надо не только знак учитывать, но и флаг переполнения. И если есть переполнение - реверсировать результат.
MOV A,N1
MOV B,N2
SUB B
MOV B,A
RRA
XOR B
JM less
В коде не совсем уверен, сейчас несколько нетрезв.
Возможно пригодится - сравнение 16 битных знаковых чисел
Чуть измененный вариант из книгиКод:;DE и HL - знаковые 16 битные целые
;Портит A и B
CmpDEHL: mov a,d
xra h
jm DiffSigns
mov a,e
sub l
mov b,a
mov a,d
sbb h
rc ;DE<HL (CY=1)
ora b
ret ;или DE>HL (CY=0;Z=0) или DE=HL (CY=0;Z=1)
DiffSigns: xra h
rp ;DE>HL (CY=0;Z=0)
stc
ret ;DE<HL (CY=1)
Григорьев В.Л. Программное обеспечение микропроцессорных систем. М.: Энергоатомиздат, 1983 (с. 173-174)
Если автор топика задумал написть свой компилятор Си под ВМ80, то может ему пригодится книга "Компилятор Си для микроЭВМ" Д.Хендрикс. В ней описывается Смолл-Си.
Вот ссылки из инета.
http://net.lg.ua/~cray/compilers/jhc.djvu
http://net.lg.ua/~cray/compilers/smallc21.rar
8-битное сравнение из 16-битного варианта вроде тоже неплохо получается:
Код:;A и E - знаковые 8 битные целые
;Кроме флагов ничего не портит
CmpAE: xra e
jm DiffSigns
xra e
cmp e
ret ;A<E (CY=1) или A>E (CY=0;Z=0) или A=E (CY=0;Z=1)
DiffSigns:
xra e
rp ;A>E (CY=0;Z=0)
stc
ret ;A<E (CY=1)
Мой вариант сравнения с инверсией флага переноса неверен.
vinxru, используй свою версию с +80. Похоже, на 7-й бит нельзя опираться.
Вчера, засыпая, отправил не в тот тред. На всякий случай повторюсь:
Ускоренные умножения и деления, вдруг пригодится:
http://www.cirsovius.de/CPM/Projekte...IV/MULDIV.html
Реализовал break, continue, return, switch, default, do {} while, union, typedef, extern, sizeof. Реализовано описание внешних функций и переменных. Функции сравнения + перехода оптимизированы. Реализован #define
---
Осталось:
Оптимизировать умножение и деление на константу.
Switch - это пока просто последовательность if, т.е. без таблицы переходов
Вставки ассемблера не реализованы.
Препроцессор не реализован: #include, #ifdef, #ifndef, #endif
Инициализации статических переменных нет Например: char data[] = { 0x10, 0x20 }; FileInfo files[] = { { "abc", 1 }, { "def", 2 } };
Контроля рекурсии нет и необходимый размер стека не определяется.
Сделать постоптимизатор ассемблера, который в том числе будет переводить программу в мненоники 8080, комплировать в BIN файл.
Режима стековых переменных нет.
В структурах нельзя будет описывать многомерные массивы. В моём компиляторе двухмерный массив - это массив указателей на одномерные массивы.
int a[5][10] - это
int* a[5];
int b[50];
a[0] = b;
a[1] = b+10;
a[2] = b+20;
a[3] = b+30;
a[4] = b+40;
Еще не сделал вычитание указателя из указателя. И с преобразованием типов не все впорядке.
---------- Post added at 12:01 ---------- Previous post was at 11:57 ----------
Так же в этой документации описано, что перед любой арифметической операцией надо 8 битные типы данных приводить к 16 битным.
в моем же случае любая операция между 8 битными значениями даст 8 битный результат. Кроме умножения, оно дает 16 бит.
А любое сравнение или операция НЕ дает 8 битный результат.
Команда XTHL пригодилась.
На команде загрузить в HL значение с верхушки стека. Но перед тем как использовать HL, надо его прошлое значение сохранить в стек.
Что то типа
POP DE
PUSH HL
EX HL, DE
И даже команда DEC SP пригодилась. Для варианта
PUSH DE
...
INC SP
POP AF
DEC SP
Сейчас сказать не могу.
Ради отладки компилятора переписал с JS игру. Почти полностью на Си.
http://s019.radikal.ru/i615/1209/b9/3875185eea1d.png