1 /*
2 * Copyright (c) 2021-2022 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16 /* This files contains faultlog sdk interface functions. */
17
18 #include "dfx_dump_catcher.h"
19
20 #include <cerrno>
21 #include <climits>
22 #include <csignal>
23 #include <cstdlib>
24 #include <cstring>
25 #include <ctime>
26 #include <memory>
27 #include <mutex>
28 #include <string>
29 #include <vector>
30
31 #include <dirent.h>
32 #include <poll.h>
33 #include <unistd.h>
34
35 #include <sys/syscall.h>
36 #include <sys/types.h>
37
38 #include "dfx_util.h"
39 #include "dfx_define.h"
40 #include "dfx_dump_res.h"
41 #include "dfx_frame.h"
42 #include "dfx_log.h"
43 #include "dfx_unwind_local.h"
44 #include "faultloggerd_client.h"
45 #include "iosfwd"
46 #include "securec.h"
47 #include "strings.h"
48 #include "file_ex.h"
49
50 static const int NUMBER_TEN = 10;
51 static const int MAX_TEMP_FILE_LENGTH = 256;
52 static const int DUMP_CATCHE_WORK_TIME_S = 60;
53 static const int NUMBER_TWO_KB = 2048;
54 static const int BACK_TRACE_DUMP_MIX_TIMEOUT_MS = 2000;
55 static const int BACK_TRACE_DUMP_CPP_TIMEOUT_MS = 10000;
56
57 namespace OHOS {
58 namespace HiviewDFX {
59 namespace {
60 static const std::string DFXDUMPCATCHER_TAG = "DfxDumpCatcher";
IsThreadInCurPid(int32_t tid)61 static bool IsThreadInCurPid(int32_t tid)
62 {
63 std::string path = "/proc/self/task/" + std::to_string(tid);
64 return access(path.c_str(), F_OK) == 0;
65 }
66 enum DfxDumpPollRes : int32_t {
67 DUMP_POLL_INIT = -1,
68 DUMP_POLL_OK,
69 DUMP_POLL_FD,
70 DUMP_POLL_FAILED,
71 DUMP_POLL_TIMEOUT,
72 DUMP_POLL_RETURN,
73 };
74 }
75
DfxDumpCatcher()76 DfxDumpCatcher::DfxDumpCatcher()
77 {
78 frameCatcherPid_ = 0;
79 (void)GetProcStatus(procInfo_);
80 }
81
DfxDumpCatcher(int32_t pid)82 DfxDumpCatcher::DfxDumpCatcher(int32_t pid) : frameCatcherPid_(pid)
83 {
84 (void)GetProcStatus(procInfo_);
85 }
86
~DfxDumpCatcher()87 DfxDumpCatcher::~DfxDumpCatcher()
88 {
89 }
90
DoDumpCurrTid(const size_t skipFramNum,std::string & msg)91 bool DfxDumpCatcher::DoDumpCurrTid(const size_t skipFramNum, std::string& msg)
92 {
93 bool ret = false;
94 int currTid = syscall(SYS_gettid);
95 size_t tmpSkipFramNum = skipFramNum + 1;
96 ret = DfxUnwindLocal::GetInstance().ExecLocalDumpUnwind(tmpSkipFramNum);
97 if (ret) {
98 msg.append(DfxUnwindLocal::GetInstance().CollectUnwindResult(currTid));
99 } else {
100 msg.append("Failed to dump curr thread:" + std::to_string(currTid) + ".\n");
101 }
102 DfxLogDebug("%s :: DoDumpCurrTid :: return %d.", DFXDUMPCATCHER_TAG.c_str(), ret);
103 return ret;
104 }
105
DoDumpLocalTid(const int tid,std::string & msg)106 bool DfxDumpCatcher::DoDumpLocalTid(const int tid, std::string& msg)
107 {
108 bool ret = false;
109 if (tid <= 0) {
110 DfxLogError("%s :: DoDumpLocalTid :: return false as param error.", DFXDUMPCATCHER_TAG.c_str());
111 return ret;
112 }
113
114 if (DfxUnwindLocal::GetInstance().SendAndWaitRequest(tid)) {
115 ret = DfxUnwindLocal::GetInstance().ExecLocalDumpUnwindByWait();
116 }
117
118 if (ret) {
119 msg.append(DfxUnwindLocal::GetInstance().CollectUnwindResult(tid));
120 } else {
121 msg.append("Failed to dump thread:" + std::to_string(tid) + ".\n");
122 }
123 DfxLogDebug("%s :: DoDumpLocalTid :: return %d.", DFXDUMPCATCHER_TAG.c_str(), ret);
124 return ret;
125 }
126
DoDumpLocalPid(int pid,std::string & msg)127 bool DfxDumpCatcher::DoDumpLocalPid(int pid, std::string& msg)
128 {
129 bool ret = false;
130 if (pid <= 0) {
131 DfxLogError("%s :: DoDumpLocalPid :: return false as param error.", DFXDUMPCATCHER_TAG.c_str());
132 return ret;
133 }
134 size_t skipFramNum = DUMP_CATCHER_NUMBER_THREE;
135
136 char realPath[PATH_MAX] = {'\0'};
137 if (realpath("/proc/self/task", realPath) == nullptr) {
138 DfxLogError("%s :: DoDumpLocalPid :: return false as realpath failed.", DFXDUMPCATCHER_TAG.c_str());
139 return ret;
140 }
141
142 DIR *dir = opendir(realPath);
143 if (dir == nullptr) {
144 DfxLogError("%s :: DoDumpLocalPid :: return false as opendir failed.", DFXDUMPCATCHER_TAG.c_str());
145 return ret;
146 }
147
148 struct dirent *ent;
149 while ((ent = readdir(dir)) != nullptr) {
150 if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
151 continue;
152 }
153
154 pid_t tid = atoi(ent->d_name);
155 if (tid == 0) {
156 continue;
157 }
158
159 int currentTid = syscall(SYS_gettid);
160 if (tid == currentTid) {
161 ret = DoDumpCurrTid(skipFramNum, msg);
162 } else {
163 ret = DoDumpLocalTid(tid, msg);
164 }
165 }
166
167 if (closedir(dir) == -1) {
168 DfxLogError("closedir failed.");
169 }
170
171 DfxLogDebug("%s :: DoDumpLocalPid :: return %d.", DFXDUMPCATCHER_TAG.c_str(), ret);
172 return ret;
173 }
174
DoDumpRemoteLocked(int pid,int tid,std::string & msg)175 bool DfxDumpCatcher::DoDumpRemoteLocked(int pid, int tid, std::string& msg)
176 {
177 int type = static_cast<int>(DUMP_TYPE_NATIVE);
178 return DoDumpCatchRemote(type, pid, tid, msg);
179 }
180
DoDumpLocalLocked(int pid,int tid,std::string & msg)181 bool DfxDumpCatcher::DoDumpLocalLocked(int pid, int tid, std::string& msg)
182 {
183 bool ret = DfxUnwindLocal::GetInstance().Init();
184 if (!ret) {
185 DfxLogError("%s :: DoDumpLocal :: Init error.", DFXDUMPCATCHER_TAG.c_str());
186 DfxUnwindLocal::GetInstance().Destroy();
187 return ret;
188 }
189
190 size_t skipFramNum = DUMP_CATCHER_NUMBER_TWO;
191 if (tid == syscall(SYS_gettid)) {
192 ret = DoDumpCurrTid(skipFramNum, msg);
193 } else if (tid == 0) {
194 ret = DoDumpLocalPid(pid, msg);
195 } else {
196 if (!IsThreadInCurPid(tid)) {
197 msg.append("tid(" + std::to_string(tid) + ") is not in pid(" + std::to_string(pid) + ").\n");
198 } else {
199 ret = DoDumpLocalTid(tid, msg);
200 }
201 }
202 DfxUnwindLocal::GetInstance().Destroy();
203 DfxLogDebug("%s :: DoDumpLocal :: ret(%d).", DFXDUMPCATCHER_TAG.c_str(), ret);
204 return ret;
205 }
206
DumpCatchMix(int pid,int tid,std::string & msg)207 bool DfxDumpCatcher::DumpCatchMix(int pid, int tid, std::string& msg)
208 {
209 int type = static_cast<int>(DUMP_TYPE_MIX);
210 return DoDumpCatchRemote(type, pid, tid, msg);
211 }
212
DumpCatch(int pid,int tid,std::string & msg)213 bool DfxDumpCatcher::DumpCatch(int pid, int tid, std::string& msg)
214 {
215 bool ret = false;
216 if (pid <= 0 || tid < 0) {
217 DfxLogError("%s :: dump_catch :: param error.", DFXDUMPCATCHER_TAG.c_str());
218 return ret;
219 }
220
221 std::unique_lock<std::mutex> lck(dumpCatcherMutex_);
222 int currentPid = getpid();
223 DfxLogDebug("%s :: dump_catch :: cPid(%d), pid(%d), tid(%d).",
224 DFXDUMPCATCHER_TAG.c_str(), currentPid, pid, tid);
225
226 if (pid == currentPid) {
227 ret = DoDumpLocalLocked(pid, tid, msg);
228 } else {
229 ret = DoDumpRemoteLocked(pid, tid, msg);
230 }
231 DfxLogDebug("%s :: dump_catch :: ret: %d, msg: %s", DFXDUMPCATCHER_TAG.c_str(), ret, msg.c_str());
232 return ret;
233 }
234
DumpCatchFd(int pid,int tid,std::string & msg,int fd)235 bool DfxDumpCatcher::DumpCatchFd(int pid, int tid, std::string& msg, int fd)
236 {
237 bool ret = false;
238 ret = DumpCatch(pid, tid, msg);
239 if (fd > 0) {
240 ret = write(fd, msg.c_str(), msg.length());
241 }
242 return ret;
243 }
244
SignalTargetProcess(const int type,int pid,int tid)245 static int SignalTargetProcess(const int type, int pid, int tid)
246 {
247 DfxLogDebug("%s :: SignalTargetProcess :: type: %d", DFXDUMPCATCHER_TAG.c_str(), type);
248 #pragma clang diagnostic push
249 #pragma clang diagnostic ignored "-Winitializer-overrides"
250 siginfo_t si = {
251 .si_signo = SIGDUMP,
252 .si_errno = 0,
253 .si_code = type,
254 .si_value.sival_int = tid,
255 .si_pid = getpid(),
256 .si_uid = static_cast<uid_t>(syscall(SYS_gettid))
257 };
258 #pragma clang diagnostic pop
259 if (tid == 0) {
260 if (syscall(SYS_rt_sigqueueinfo, pid, si.si_signo, &si) != 0) {
261 return errno;
262 }
263 } else {
264 if (syscall(SYS_rt_tgsigqueueinfo, pid, tid, si.si_signo, &si) != 0) {
265 return errno;
266 }
267 }
268 return 0;
269 }
270
LoadPathContent(const std::string & desc,const std::string & path,std::string & result)271 static void LoadPathContent(const std::string& desc, const std::string& path, std::string& result)
272 {
273 if (access(path.c_str(), F_OK) != 0) {
274 result.append("Target path(");
275 result.append(path);
276 result.append(") is not exist. errno(");
277 result.append(std::to_string(errno));
278 result.append(").\n");
279 return;
280 }
281
282 std::string content;
283 LoadStringFromFile(path, content);
284 if (!content.empty()) {
285 std::string str = desc + ":\n" + content + "\n";
286 result.append(str);
287 }
288 return;
289 }
290
LoadPidStat(const int pid,std::string & msg)291 static void LoadPidStat(const int pid, std::string& msg)
292 {
293 std::string statPath = "/proc/" + std::to_string(pid) + "/stat";
294 std::string wchanPath = "/proc/" + std::to_string(pid) + "/wchan";
295 LoadPathContent("stat", statPath, msg);
296 LoadPathContent("wchan", wchanPath, msg);
297 }
298
DoDumpCatchRemote(const int type,int pid,int tid,std::string & msg)299 bool DfxDumpCatcher::DoDumpCatchRemote(const int type, int pid, int tid, std::string& msg)
300 {
301 bool ret = false;
302 if (pid <= 0 || tid < 0) {
303 msg.append("Result: pid(" + std::to_string(pid) + ") param error.\n");
304 DfxLogWarn("%s :: %s :: %s", DFXDUMPCATCHER_TAG.c_str(), __func__, msg.c_str());
305 return ret;
306 }
307
308 int sdkdumpRet = RequestSdkDump(type, pid, tid);
309 if (sdkdumpRet != (int)FaultLoggerSdkDumpResp::SDK_DUMP_PASS) {
310 if (sdkdumpRet == (int)FaultLoggerSdkDumpResp::SDK_DUMP_REPEAT) {
311 msg.append("Result: pid(" + std::to_string(pid) + ") is dumping.\n");
312 DfxLogWarn("%s :: %s :: %s", DFXDUMPCATCHER_TAG.c_str(), __func__, msg.c_str());
313 return ret;
314 } else {
315 if (sdkdumpRet == (int)FaultLoggerSdkDumpResp::SDK_DUMP_REJECT) {
316 msg.append("Result: pid(" + std::to_string(pid) + ") check permission error.\n");
317 }
318 int err = SignalTargetProcess(type, pid, tid);
319 if (err != 0) {
320 msg.append("Result: pid(" + std::to_string(pid) + ") syscall SIGDUMP error, \
321 errno(" + std::to_string(err) + ").\n");
322 DfxLogWarn("%s :: %s :: %s", DFXDUMPCATCHER_TAG.c_str(), __func__, msg.c_str());
323 RequestDelPipeFd(pid);
324 return ret;
325 }
326 }
327 }
328
329 int pollRet = DoDumpRemotePid(type, pid, msg);
330 switch (pollRet) {
331 case DUMP_POLL_OK:
332 ret = true;
333 break;
334 case DUMP_POLL_TIMEOUT:
335 if (type == DUMP_TYPE_MIX) {
336 msg.append("Result: pid(" + std::to_string(pid) + ") dump mix timeout, try dump native frame.\n");
337 int type = static_cast<int>(DUMP_TYPE_NATIVE);
338 DoDumpCatchRemote(type, pid, tid, msg);
339 } else if (type == DUMP_TYPE_NATIVE) {
340 LoadPidStat(pid, msg);
341 }
342 break;
343 default:
344 break;
345 }
346 DfxLogInfo("%s :: %s :: ret: %d", DFXDUMPCATCHER_TAG.c_str(), __func__, ret);
347 return ret;
348 }
349
DoDumpRemotePid(const int type,int pid,std::string & msg)350 int DfxDumpCatcher::DoDumpRemotePid(const int type, int pid, std::string& msg)
351 {
352 int readBufFd = RequestPipeFd(pid, FaultLoggerPipeType::PIPE_FD_READ_BUF);
353 DfxLogDebug("read buf fd: %d", readBufFd);
354
355 int readResFd = RequestPipeFd(pid, FaultLoggerPipeType::PIPE_FD_READ_RES);
356 DfxLogDebug("read res fd: %d", readResFd);
357
358 int timeout = BACK_TRACE_DUMP_CPP_TIMEOUT_MS;
359 if (type == DUMP_TYPE_MIX) {
360 timeout = BACK_TRACE_DUMP_MIX_TIMEOUT_MS;
361 }
362
363 int ret = DoDumpRemotePoll(readBufFd, readResFd, timeout, msg);
364
365 // request close fds in faultloggerd
366 RequestDelPipeFd(pid);
367 if (readBufFd >= 0) {
368 close(readBufFd);
369 readBufFd = -1;
370 }
371 if (readResFd >= 0) {
372 close(readResFd);
373 readResFd = -1;
374 }
375 DfxLogInfo("%s :: %s :: ret: %d", DFXDUMPCATCHER_TAG.c_str(), __func__, ret);
376 return ret;
377 }
378
DoDumpRemotePoll(int bufFd,int resFd,int timeout,std::string & msg)379 int DfxDumpCatcher::DoDumpRemotePoll(int bufFd, int resFd, int timeout, std::string& msg)
380 {
381 int ret = DUMP_POLL_INIT;
382 bool res = false;
383 std::string bufMsg;
384 std::string resMsg;
385 struct pollfd readfds[2];
386 (void)memset_s(readfds, sizeof(readfds), 0, sizeof(readfds));
387 readfds[0].fd = bufFd;
388 readfds[0].events = POLLIN;
389 readfds[1].fd = resFd;
390 readfds[1].events = POLLIN;
391 int fdsSize = sizeof(readfds) / sizeof(readfds[0]);
392 bool bPipeConnect = false;
393 while (true) {
394 if (bufFd < 0 || resFd < 0) {
395 ret = DUMP_POLL_FD;
396 resMsg.append("Result: bufFd or resFd < 0.\n");
397 break;
398 }
399
400 int pollRet = poll(readfds, fdsSize, timeout);
401 if (pollRet < 0) {
402 ret = DUMP_POLL_FAILED;
403 resMsg.append("Result: poll error, errno(" + std::to_string(errno) + ")\n");
404 break;
405 } else if (pollRet == 0) {
406 ret = DUMP_POLL_TIMEOUT;
407 resMsg.append("Result: poll timeout.\n");
408 break;
409 }
410
411 bool bufRet = true;
412 bool resRet = false;
413 bool eventRet = true;
414 for (int i = 0; i < fdsSize; ++i) {
415 if (!bPipeConnect && ((uint32_t)readfds[i].revents & POLLIN)) {
416 bPipeConnect = true;
417 }
418 if (bPipeConnect &&
419 (((uint32_t)readfds[i].revents & POLLERR) || ((uint32_t)readfds[i].revents & POLLHUP))) {
420 eventRet = false;
421 resMsg.append("Result: poll events error.\n");
422 break;
423 }
424
425 if (((uint32_t)readfds[i].revents & POLLIN) != POLLIN) {
426 continue;
427 }
428
429 if (readfds[i].fd == bufFd) {
430 bufRet = DoReadBuf(bufFd, bufMsg);
431 }
432
433 if (readfds[i].fd == resFd) {
434 resRet = DoReadRes(resFd, res, resMsg);
435 }
436 }
437
438 if ((eventRet == false) || (bufRet == false) || (resRet == true)) {
439 ret = DUMP_POLL_RETURN;
440 break;
441 }
442 }
443
444 DfxLogInfo("%s :: %s :: %s", DFXDUMPCATCHER_TAG.c_str(), __func__, resMsg.c_str());
445 msg = resMsg + bufMsg;
446 if (res) {
447 ret = DUMP_POLL_OK;
448 }
449 return ret;
450 }
451
DoReadBuf(int fd,std::string & msg)452 bool DfxDumpCatcher::DoReadBuf(int fd, std::string& msg)
453 {
454 char buffer[LOG_BUF_LEN];
455 bzero(buffer, sizeof(buffer));
456 ssize_t nread = read(fd, buffer, sizeof(buffer) - 1);
457 if (nread <= 0) {
458 DfxLogWarn("%s :: %s :: read error", DFXDUMPCATCHER_TAG.c_str(), __func__);
459 return false;
460 }
461 msg.append(buffer);
462 return true;
463 }
464
DoReadRes(int fd,bool & ret,std::string & msg)465 bool DfxDumpCatcher::DoReadRes(int fd, bool &ret, std::string& msg)
466 {
467 DumpResMsg dumpRes;
468 ssize_t nread = read(fd, &dumpRes, sizeof(struct DumpResMsg));
469 if (nread != sizeof(struct DumpResMsg)) {
470 DfxLogWarn("%s :: %s :: read error", DFXDUMPCATCHER_TAG.c_str(), __func__);
471 return false;
472 }
473
474 if (dumpRes.res == ProcessDumpRes::DUMP_ESUCCESS) {
475 ret = true;
476 }
477 DfxDumpRes::GetInstance().SetRes(dumpRes.res);
478 msg.append("Result: " + DfxDumpRes::GetInstance().ToString() + "\n");
479 return true;
480 }
481
DumpCatchMultiPid(const std::vector<int> pidV,std::string & msg)482 bool DfxDumpCatcher::DumpCatchMultiPid(const std::vector<int> pidV, std::string& msg)
483 {
484 bool ret = false;
485 int pidSize = (int)pidV.size();
486 if (pidSize <= 0) {
487 DfxLogError("%s :: %s :: param error, pidSize(%d).", DFXDUMPCATCHER_TAG.c_str(), __func__, pidSize);
488 return ret;
489 }
490
491 std::unique_lock<std::mutex> lck(dumpCatcherMutex_);
492 int currentPid = getpid();
493 int currentTid = syscall(SYS_gettid);
494 DfxLogDebug("%s :: %s :: cPid(%d), cTid(%d), pidSize(%d).", DFXDUMPCATCHER_TAG.c_str(), \
495 __func__, currentPid, currentTid, pidSize);
496
497 time_t startTime = time(nullptr);
498 if (startTime > 0) {
499 DfxLogDebug("%s :: %s :: startTime(%lld).", DFXDUMPCATCHER_TAG.c_str(), __func__, startTime);
500 }
501
502 for (int i = 0; i < pidSize; i++) {
503 int pid = pidV[i];
504 std::string pidStr;
505 if (DoDumpRemoteLocked(pid, 0, pidStr)) {
506 msg.append(pidStr + "\n");
507 } else {
508 msg.append("Failed to dump process:" + std::to_string(pid));
509 }
510
511 time_t currentTime = time(nullptr);
512 if (currentTime > 0) {
513 DfxLogDebug("%s :: %s :: startTime(%lld), currentTime(%lld).", DFXDUMPCATCHER_TAG.c_str(), \
514 __func__, startTime, currentTime);
515 if (currentTime > startTime + DUMP_CATCHE_WORK_TIME_S) {
516 break;
517 }
518 }
519 }
520
521 DfxLogDebug("%s :: %s :: msg(%s).", DFXDUMPCATCHER_TAG.c_str(), __func__, msg.c_str());
522 if (msg.find("Tid:") != std::string::npos) {
523 ret = true;
524 }
525 return ret;
526 }
527
InitFrameCatcher()528 bool DfxDumpCatcher::InitFrameCatcher()
529 {
530 std::unique_lock<std::mutex> lck(dumpCatcherMutex_);
531 bool ret = DfxUnwindLocal::GetInstance().Init();
532 if (!ret) {
533 DfxUnwindLocal::GetInstance().Destroy();
534 }
535 return ret;
536 }
537
DestroyFrameCatcher()538 void DfxDumpCatcher::DestroyFrameCatcher()
539 {
540 std::unique_lock<std::mutex> lck(dumpCatcherMutex_);
541 DfxUnwindLocal::GetInstance().Destroy();
542 }
543
RequestCatchFrame(int tid)544 bool DfxDumpCatcher::RequestCatchFrame(int tid)
545 {
546 if (tid == procInfo_.tid) {
547 return true;
548 }
549 return DfxUnwindLocal::GetInstance().SendAndWaitRequest(tid);
550 }
551
CatchFrame(int tid,std::vector<std::shared_ptr<DfxFrame>> & frames)552 bool DfxDumpCatcher::CatchFrame(int tid, std::vector<std::shared_ptr<DfxFrame>>& frames)
553 {
554 if (tid <= 0 || frameCatcherPid_ != procInfo_.pid) {
555 DfxLogError("DfxDumpCatchFrame :: only support localDump.");
556 return false;
557 }
558
559 if (!IsThreadInCurPid(tid) && !procInfo_.ns) {
560 DfxLogError("DfxDumpCatchFrame :: target tid is not in our task.");
561 return false;
562 }
563 size_t skipFramNum = DUMP_CATCHER_NUMBER_ONE;
564
565 std::unique_lock<std::mutex> lck(dumpCatcherMutex_);
566 if (tid == procInfo_.tid) {
567 if (!DfxUnwindLocal::GetInstance().ExecLocalDumpUnwind(skipFramNum)) {
568 DfxLogError("DfxDumpCatchFrame :: failed to unwind for current thread(%d).", tid);
569 return false;
570 }
571 } else if (!DfxUnwindLocal::GetInstance().ExecLocalDumpUnwindByWait()) {
572 DfxLogError("DfxDumpCatchFrame :: failed to unwind for thread(%d).", tid);
573 return false;
574 }
575
576 DfxUnwindLocal::GetInstance().CollectUnwindFrames(frames);
577 return true;
578 }
579 } // namespace HiviewDFX
580 } // namespace OHOS
581